Add Hahomoe provider
This commit is contained in:
parent
8a1039db8e
commit
ea21bfed20
|
@ -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%"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.lagradost"/>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ subprojects {
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.3.2") // http library
|
implementation("com.github.Blatzar:NiceHttp:0.3.2") // http library
|
||||||
implementation("org.jsoup:jsoup:1.13.1") // html parser
|
implementation("org.jsoup:jsoup:1.13.1") // html parser
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||||
|
implementation("io.karn:khttp-android:0.1.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue