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") } }