Add Hahomoe provider

This commit is contained in:
Jace 2022-08-23 11:26:20 +08:00
parent 8a1039db8e
commit ea21bfed20
5 changed files with 332 additions and 0 deletions

26
Hahomoe/build.gradle.kts Normal file
View File

@ -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%"
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.lagradost"/>

View File

@ -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<TvType> 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<HomePageList>()
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<SearchResponse> {
val items = soup.select("ul.thumb > li > a")
if (items.isEmpty()) return ArrayList()
val returnValue = ArrayList<SearchResponse>()
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<SearchResponse> {
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<ExtractorLink>()
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
}
}

View File

@ -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())
}
}

View File

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