From ea21bfed2086041865ccc32ee90ff5c6315cc1d8 Mon Sep 17 00:00:00 2001
From: Jace <54625750+Jacekun@users.noreply.github.com>
Date: Tue, 23 Aug 2022 11:26:20 +0800
Subject: [PATCH] Add Hahomoe provider
---
Hahomoe/build.gradle.kts | 26 ++
Hahomoe/src/main/AndroidManifest.xml | 2 +
.../src/main/kotlin/com/jacekun/Hahomoe.kt | 290 ++++++++++++++++++
.../main/kotlin/com/jacekun/HahomoePlugin.kt | 13 +
build.gradle.kts | 1 +
5 files changed, 332 insertions(+)
create mode 100644 Hahomoe/build.gradle.kts
create mode 100644 Hahomoe/src/main/AndroidManifest.xml
create mode 100644 Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
create mode 100644 Hahomoe/src/main/kotlin/com/jacekun/HahomoePlugin.kt
diff --git a/Hahomoe/build.gradle.kts b/Hahomoe/build.gradle.kts
new file mode 100644
index 0000000..6d826de
--- /dev/null
+++ b/Hahomoe/build.gradle.kts
@@ -0,0 +1,26 @@
+// use an integer for version numbers
+version = 1
+
+
+cloudstream {
+ // All of these properties are optional, you can safely remove them
+
+ description = ""
+ authors = listOf("ArjixWasTaken")
+
+ /**
+ * Status int as the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta only
+ * */
+ status = 1 // will be 3 if unspecified
+
+ // List of video source types. Users are able to filter for extensions in a given category.
+ // You can find a list of avaliable types here:
+ // https://recloudstream.github.io/cloudstream/html/app/com.lagradost.cloudstream3/-tv-type/index.html
+ tvTypes = listOf("NSFW")
+
+ iconUrl = "https://www.google.com/s2/favicons?domain=haho.moe&sz=%size%"
+}
diff --git a/Hahomoe/src/main/AndroidManifest.xml b/Hahomoe/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aec9d
--- /dev/null
+++ b/Hahomoe/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt b/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
new file mode 100644
index 0000000..2b608cb
--- /dev/null
+++ b/Hahomoe/src/main/kotlin/com/jacekun/Hahomoe.kt
@@ -0,0 +1,290 @@
+package com.jacekun
+
+import android.annotation.SuppressLint
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.text.SimpleDateFormat
+import java.util.*
+import khttp.structures.cookie.CookieJar
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+
+//Credits https://raw.githubusercontent.com/ArjixWasTaken/CloudStream-3/master/app/src/main/java/com/ArjixWasTaken/cloudstream3/animeproviders/HahoMoeProvider.kt
+
+class Hahomoe : MainAPI() {
+ companion object {
+ var token: String? = null
+ var cookie: CookieJar? = null
+
+ fun getType(t: String): TvType {
+ return TvType.NSFW
+ /*
+ return if (t.contains("OVA") || t.contains("Special")) TvType.OVA
+ else if (t.contains("Movie")) TvType.AnimeMovie else TvType.Anime
+ */
+ }
+ }
+ private val globalTvType = TvType.NSFW
+ override var mainUrl = "https://haho.moe"
+ override var name = "Haho moe"
+ override val hasQuickSearch: Boolean get() = false
+ override val hasMainPage: Boolean get() = true
+ override val supportedTypes: Set get() = setOf(TvType.NSFW)
+
+ private fun loadToken(): Boolean {
+ return try {
+ val response = khttp.get(mainUrl)
+ cookie = response.cookies
+ val document = Jsoup.parse(response.text)
+ token = document.selectFirst("""meta[name="csrf-token"]""")?.attr("content")
+ token != null
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ override suspend fun getMainPage(
+ page: Int,
+ request: MainPageRequest
+ ): HomePageResponse {
+ val items = ArrayList()
+ val soup = app.get(mainUrl).document
+ for (section in soup.select("#content > section")) {
+ try {
+ if (section.attr("id") == "toplist-tabs") {
+ for (top in section.select(".tab-content > [role=\"tabpanel\"]")) {
+ val title = "Top - " + top.attr("id").split("-")[1].uppercase(Locale.UK)
+ val anime =
+ top.select("li > a").mapNotNull {
+ val epTitle = it.selectFirst(".thumb-title")?.text() ?: ""
+ val url = fixUrlNull(it?.attr("href")) ?: return@mapNotNull null
+ AnimeSearchResponse(
+ name = epTitle,
+ url = url,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = it.selectFirst("img")?.attr("src"),
+ dubStatus = EnumSet.of(DubStatus.Subbed),
+ )
+ }
+ items.add(HomePageList(title, anime))
+ }
+ } else {
+ val title = section.selectFirst("h2")?.text() ?: ""
+ val anime =
+ section.select("li > a").mapNotNull {
+ val epTitle = it.selectFirst(".thumb-title")?.text() ?: ""
+ val url = fixUrlNull(it?.attr("href")) ?: return@mapNotNull null
+ AnimeSearchResponse(
+ name = epTitle,
+ url = url,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = it.selectFirst("img")?.attr("src"),
+ dubStatus = EnumSet.of(DubStatus.Subbed),
+ )
+ }
+ items.add(HomePageList(title, anime))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ if (items.size <= 0) throw ErrorLoadingException()
+ return HomePageResponse(items)
+ }
+
+ private fun getIsMovie(type: String, id: Boolean = false): Boolean {
+ if (!id) return type == "Movie"
+
+ val movies = listOf("rrso24fa", "e4hqvtym", "bl5jdbqn", "u4vtznut", "37t6h2r4", "cq4azcrj")
+ val aniId = type.replace("$mainUrl/anime/", "")
+ return movies.contains(aniId)
+ }
+
+ private fun parseSearchPage(soup: Document): ArrayList {
+ val items = soup.select("ul.thumb > li > a")
+ if (items.isEmpty()) return ArrayList()
+ val returnValue = ArrayList()
+ for (i in items) {
+ val href = fixUrlNull(i.attr("href")) ?: ""
+ val img = fixUrlNull(i.selectFirst("img")?.attr("src"))
+ val title = i.attr("title")
+ if (href.isNotBlank()) {
+ returnValue.add(
+ if (getIsMovie(href, true)) {
+ MovieSearchResponse(
+ name = title,
+ url = href,
+ apiName = this.name,
+ type = globalTvType,
+ )
+ } else {
+ AnimeSearchResponse(
+ name = title,
+ url = href,
+ apiName = this.name,
+ type = globalTvType,
+ posterUrl = img,
+ dubStatus = EnumSet.of(DubStatus.Subbed),
+ )
+ }
+ )
+ }
+ }
+ return returnValue
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ private fun dateParser(dateString: String): String? {
+ try {
+ val format = SimpleDateFormat("dd 'of' MMM',' yyyy")
+ val newFormat = SimpleDateFormat("dd-MM-yyyy")
+ val data =
+ format.parse(
+ dateString
+ .replace("th ", " ")
+ .replace("st ", " ")
+ .replace("nd ", " ")
+ .replace("rd ", " ")
+ )
+ ?: return null
+ return newFormat.format(data)
+ } catch (e: Exception) {
+ return null
+ }
+ }
+
+ override suspend fun search(query: String): ArrayList {
+ val url = "$mainUrl/anime"
+ var response =
+ app.get(
+ url,
+ params = mapOf("q" to query),
+ cookies = mapOf("loop-view" to "thumb")
+ )
+ var document = Jsoup.parse(response.text)
+ val returnValue = parseSearchPage(document)
+
+ while (document.select("""a.page-link[rel="next"]""").isNullOrEmpty()) {
+ val link = document.select("""a.page-link[rel="next"]""")
+ if (link.isNotEmpty()) {
+ response = app.get(link[0].attr("href"), cookies = mapOf("loop-view" to "thumb"))
+ document = Jsoup.parse(response.text)
+ returnValue.addAll(parseSearchPage(document))
+ } else {
+ break
+ }
+ }
+
+ return returnValue
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val document = app.get(url, cookies = mapOf("loop-view" to "thumb")).document
+
+ val englishTitle =
+ document.selectFirst("span.value > span[title=\"English\"]")
+ ?.parent()
+ ?.text()
+ ?.trim()
+ val japaneseTitle =
+ document.selectFirst("span.value > span[title=\"Japanese\"]")
+ ?.parent()
+ ?.text()
+ ?.trim()
+ val canonicalTitle = document.selectFirst("header.entry-header > h1.mb-3")?.text()?.trim()
+
+ val episodeNodes = document.select("li[class*=\"episode\"] > a")
+
+ val episodes = episodeNodes.mapNotNull {
+ val dataUrl = it?.attr("href") ?: return@mapNotNull null
+ val epi = Episode(
+ data = dataUrl,
+ name = it.selectFirst(".episode-title")?.text()?.trim(),
+ posterUrl = it.selectFirst("img")?.attr("src"),
+ description = it.attr("data-content").trim(),
+ )
+ epi.addDate(it.selectFirst(".episode-date")?.text()?.trim())
+ epi
+ }
+ val status =
+ when (document.selectFirst("li.status > .value")?.text()?.trim()) {
+ "Ongoing" -> ShowStatus.Ongoing
+ "Completed" -> ShowStatus.Completed
+ else -> null
+ }
+ val yearText = document.selectFirst("li.release-date .value")?.text() ?: ""
+ val pattern = "(\\d{4})".toRegex()
+ val (year) = pattern.find(yearText)!!.destructured
+
+ val poster = document.selectFirst("img.cover-image")?.attr("src")
+ val type = document.selectFirst("a[href*=\"$mainUrl/type/\"]")?.text()?.trim()
+
+ val synopsis = document.selectFirst(".entry-description > .card-body")?.text()?.trim()
+ val genre =
+ document.select("li.genre.meta-data > span.value").map {
+ it?.text()?.trim().toString()
+ }
+
+ val synonyms =
+ document.select("li.synonym.meta-data > div.info-box > span.value").map {
+ it?.text()?.trim().toString()
+ }
+
+ return AnimeLoadResponse(
+ englishTitle,
+ japaneseTitle,
+ canonicalTitle ?: "",
+ url,
+ this.name,
+ getType(type ?: ""),
+ poster,
+ year.toIntOrNull(),
+ hashMapOf(DubStatus.Subbed to episodes),
+ status,
+ synopsis,
+ ArrayList(genre),
+ ArrayList(synonyms),
+ null,
+ null,
+ )
+ }
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+ val soup = app.get(data).document
+
+ val sources = ArrayList()
+ for (source in soup.select("""[aria-labelledby="mirror-dropdown"] > li > a.dropdown-item""")) {
+ val release = source.text().replace("/", "").trim()
+ val sourceSoup = app.get(
+ "$mainUrl/embed?v=${source.attr("href").split("v=")[1].split("&")[0]}",
+ headers=mapOf("Referer" to data)
+ ).document
+
+ for (quality in sourceSoup.select("video#player > source")) {
+ sources.add(
+ ExtractorLink(
+ this.name,
+ "${this.name} $release - " + quality.attr("title"),
+ fixUrl(quality.attr("src")),
+ this.mainUrl,
+ getQualityFromName(quality.attr("title"))
+ )
+ )
+ }
+ }
+
+ for (source in sources) {
+ callback.invoke(source)
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/Hahomoe/src/main/kotlin/com/jacekun/HahomoePlugin.kt b/Hahomoe/src/main/kotlin/com/jacekun/HahomoePlugin.kt
new file mode 100644
index 0000000..232a8fe
--- /dev/null
+++ b/Hahomoe/src/main/kotlin/com/jacekun/HahomoePlugin.kt
@@ -0,0 +1,13 @@
+package com.jacekun
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class HahomoePlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(Hahomoe())
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index db7d2f6..b6a0ed2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -81,6 +81,7 @@ subprojects {
implementation("com.github.Blatzar:NiceHttp:0.3.2") // http library
implementation("org.jsoup:jsoup:1.13.1") // html parser
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
+ implementation("io.karn:khttp-android:0.1.2")
}
}