2023-06-29 13:47:08 +00:00
|
|
|
package com.hexated
|
|
|
|
|
2023-07-13 16:34:46 +00:00
|
|
|
import com.hexated.AnichiExtractors.invokeInternalSources
|
2023-06-29 13:47:08 +00:00
|
|
|
import com.lagradost.cloudstream3.*
|
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
|
2023-06-30 05:51:14 +00:00
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
2023-06-29 13:47:08 +00:00
|
|
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
2023-08-12 17:42:08 +00:00
|
|
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
2023-06-29 13:47:08 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.*
|
|
|
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
|
|
import com.lagradost.nicehttp.RequestBodyTypes
|
|
|
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
|
|
import okhttp3.RequestBody.Companion.toRequestBody
|
|
|
|
|
2023-07-13 16:34:46 +00:00
|
|
|
open class Anichi : MainAPI() {
|
2023-06-29 13:47:08 +00:00
|
|
|
override var name = "Anichi"
|
|
|
|
override val instantLinkLoading = true
|
|
|
|
override val hasQuickSearch = false
|
|
|
|
override val hasMainPage = true
|
|
|
|
|
|
|
|
private fun getStatus(t: String): ShowStatus {
|
|
|
|
return when (t) {
|
|
|
|
"Finished" -> ShowStatus.Completed
|
|
|
|
"Releasing" -> ShowStatus.Ongoing
|
|
|
|
else -> ShowStatus.Completed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-12 17:42:08 +00:00
|
|
|
override val supportedSyncNames = setOf(SyncIdName.Anilist, SyncIdName.MyAnimeList)
|
2023-06-29 13:47:08 +00:00
|
|
|
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie)
|
|
|
|
|
|
|
|
private val popularTitle = "Popular"
|
2023-07-02 16:05:21 +00:00
|
|
|
private val animeRecentTitle = "Latest Anime"
|
|
|
|
private val donghuaRecentTitle = "Latest Donghua"
|
|
|
|
private val movieTitle = "Movie"
|
|
|
|
|
|
|
|
override val mainPage = mainPageOf(
|
|
|
|
"""$apiUrl?variables={"search":{"sortBy":"Latest_Update","allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false},"limit":26,"page":%d,"translationType":"sub","countryOrigin":"JP"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" to animeRecentTitle,
|
|
|
|
"""$apiUrl?variables={"search":{"sortBy":"Latest_Update","allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false},"limit":26,"page":%d,"translationType":"sub","countryOrigin":"CN"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}""" to donghuaRecentTitle,
|
|
|
|
"""$apiUrl?variables={"type":"anime","size":30,"dateRange":1,"page":%d,"allowAdult":${settingsForProvider.enableAdult},"allowUnknown":false}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$popularHash"}}""" to popularTitle,
|
|
|
|
"""$apiUrl?variables={"search":{"slug":"movie-anime","format":"anime","tagType":"upcoming","name":"Trending Movies"}}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$slugHash"}}""" to movieTitle
|
2023-06-29 13:47:08 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
|
|
|
|
|
|
|
val url = request.data.format(page)
|
2023-07-02 16:05:21 +00:00
|
|
|
val res = app.get(url, headers = headers).parsedSafe<AnichiQuery>()?.data
|
|
|
|
val query = res?.shows ?: res?.queryPopular ?: res?.queryListForTag
|
|
|
|
val card = if(request.name == popularTitle) query?.recommendations?.map { it.anyCard } else query?.edges
|
2023-07-02 16:25:42 +00:00
|
|
|
val home = card?.filter {
|
|
|
|
// filtering in case there is an anime with 0 episodes available on the site.
|
|
|
|
!(it?.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0)
|
|
|
|
}?.mapNotNull { media ->
|
2023-07-02 16:05:21 +00:00
|
|
|
media?.toSearchResponse()
|
|
|
|
} ?: emptyList()
|
|
|
|
return newHomePageResponse(
|
|
|
|
list = HomePageList(
|
|
|
|
name = request.name,
|
|
|
|
list = home,
|
|
|
|
),
|
|
|
|
hasNext = request.name != movieTitle
|
|
|
|
)
|
|
|
|
}
|
2023-06-29 13:47:08 +00:00
|
|
|
|
2023-07-02 16:05:21 +00:00
|
|
|
private fun Edges.toSearchResponse(): AnimeSearchResponse? {
|
|
|
|
|
|
|
|
return newAnimeSearchResponse(
|
|
|
|
name ?: englishName ?: nativeName ?: "",
|
|
|
|
Id ?: return null,
|
|
|
|
fix = false
|
|
|
|
) {
|
|
|
|
this.posterUrl = thumbnail
|
|
|
|
this.year = airedStart?.year
|
|
|
|
this.otherName = englishName
|
|
|
|
addDub(availableEpisodes?.dub)
|
|
|
|
addSub(availableEpisodes?.sub)
|
2023-06-29 13:47:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-02 16:05:21 +00:00
|
|
|
override suspend fun search(query: String): List<SearchResponse>? {
|
2023-06-29 13:47:08 +00:00
|
|
|
|
|
|
|
val link =
|
|
|
|
"""$apiUrl?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"$query"},"limit":26,"page":1,"translationType":"sub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}"""
|
|
|
|
val res = app.get(
|
|
|
|
link,
|
|
|
|
headers = headers
|
|
|
|
).text.takeUnless { it.contains("PERSISTED_QUERY_NOT_FOUND") }
|
|
|
|
// Retries
|
|
|
|
?: app.get(
|
|
|
|
link,
|
|
|
|
headers = headers
|
|
|
|
).text.takeUnless { it.contains("PERSISTED_QUERY_NOT_FOUND") }
|
|
|
|
?: return emptyList()
|
|
|
|
|
|
|
|
val response = parseJson<AnichiQuery>(res)
|
|
|
|
|
2023-07-02 16:05:21 +00:00
|
|
|
val results = response.data?.shows?.edges?.filter {
|
2023-06-29 13:47:08 +00:00
|
|
|
// filtering in case there is an anime with 0 episodes available on the site.
|
|
|
|
!(it.availableEpisodes?.raw == 0 && it.availableEpisodes.sub == 0 && it.availableEpisodes.dub == 0)
|
|
|
|
}
|
|
|
|
|
2023-07-02 16:05:21 +00:00
|
|
|
return results?.map {
|
2023-06-29 13:47:08 +00:00
|
|
|
newAnimeSearchResponse(it.name ?: "", "${it.Id}", fix = false) {
|
|
|
|
this.posterUrl = it.thumbnail
|
|
|
|
this.year = it.airedStart?.year
|
|
|
|
this.otherName = it.englishName
|
|
|
|
addDub(it.availableEpisodes?.dub)
|
|
|
|
addSub(it.availableEpisodes?.sub)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-12 17:42:08 +00:00
|
|
|
override suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
|
|
|
|
val syncId = id.split("/").last()
|
|
|
|
val malId = if (name == SyncIdName.MyAnimeList) {
|
|
|
|
syncId
|
|
|
|
} else {
|
|
|
|
aniToMal(syncId)
|
|
|
|
}
|
|
|
|
|
|
|
|
val media = app.get("$jikanApi/anime/$malId").parsedSafe<JikanResponse>()?.data
|
|
|
|
val link =
|
|
|
|
"""$apiUrl?variables={"search":{"allowAdult":false,"allowUnknown":false,"query":"${media?.title}"},"limit":26,"page":1,"translationType":"sub","countryOrigin":"ALL"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"$mainHash"}}"""
|
|
|
|
val res = app.get(
|
|
|
|
link,
|
|
|
|
headers = headers
|
|
|
|
).parsedSafe<AnichiQuery>()?.data?.shows?.edges
|
|
|
|
return res?.find {
|
|
|
|
(it.name.equals(media?.title, true) || it.englishName.equals(
|
|
|
|
media?.title_english,
|
|
|
|
true
|
|
|
|
) || it.nativeName.equals(media?.title_japanese, true)) && it.airedStart?.year == media?.year
|
|
|
|
}?.Id
|
|
|
|
}
|
|
|
|
|
2023-06-29 13:47:08 +00:00
|
|
|
override suspend fun load(url: String): LoadResponse? {
|
|
|
|
|
|
|
|
val id = url.substringAfterLast("/")
|
|
|
|
// lazy to format
|
|
|
|
val body = """
|
|
|
|
{
|
|
|
|
"query": " query(\n ${'$'}_id: String!\n ) {\n show(\n _id: ${'$'}_id\n ) {\n _id\n name\n description\n thumbnail\n thumbnails\n lastEpisodeInfo\n lastEpisodeDate \n type\n genres\n score\n status\n season\n altNames \n averageScore\n rating\n episodeCount\n episodeDuration\n broadcastInterval\n banner\n airedEnd\n airedStart \n studios\n characters\n availableEpisodesDetail\n availableEpisodes\n prevideos\n nameOnlyString\n relatedShows\n relatedMangas\n musics\n isAdult\n \n tags\n countryOfOrigin\n\n pageStatus{\n _id\n notes\n pageId\n showId\n \n # ranks:[Object]\n views\n likesCount\n commentCount\n dislikesCount\n reviewCount\n userScoreCount\n userScoreTotalValue\n userScoreAverValue\n viewers{\n firstViewers{\n viewCount\n lastWatchedDate\n user{\n _id\n displayName\n picture\n # description\n hideMe\n # createdAt\n # badges\n brief\n }\n \n }\n recViewers{\n viewCount\n lastWatchedDate\n user{\n _id\n displayName\n picture\n # description\n hideMe\n # createdAt\n # badges\n brief\n }\n \n }\n }\n\n }\n }\n }",
|
|
|
|
"extensions": "{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$detailHash\"}}",
|
|
|
|
"variables": "{\"_id\":\"$id\"}"
|
|
|
|
}
|
|
|
|
""".trimIndent().trim().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
|
|
|
|
|
|
|
val res = app.post(apiUrl, requestBody = body, headers = headers)
|
|
|
|
val showData = res.parsedSafe<Detail>()?.data?.show ?: return null
|
|
|
|
|
|
|
|
val title = showData.name
|
|
|
|
val description = showData.description
|
|
|
|
val poster = showData.thumbnail
|
|
|
|
|
2023-07-13 16:34:46 +00:00
|
|
|
val trackers = getTracker(
|
|
|
|
title,
|
|
|
|
showData.altNames?.firstOrNull(),
|
|
|
|
showData.airedStart?.year,
|
|
|
|
showData.season?.quarter,
|
|
|
|
showData.type
|
|
|
|
)
|
|
|
|
|
2023-07-02 17:26:33 +00:00
|
|
|
val episodes = showData.availableEpisodesDetail.let {
|
2023-06-29 13:47:08 +00:00
|
|
|
if (it == null) return@let Pair(null, null)
|
|
|
|
if (showData.Id == null) return@let Pair(null, null)
|
2023-07-02 17:26:33 +00:00
|
|
|
Pair(
|
2023-07-13 16:34:46 +00:00
|
|
|
it.getEpisode("sub", showData.Id, trackers?.idMal),
|
|
|
|
it.getEpisode("dub", showData.Id, trackers?.idMal),
|
2023-07-02 17:26:33 +00:00
|
|
|
)
|
2023-06-29 13:47:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
val characters = showData.characters?.map {
|
|
|
|
val role = when (it.role) {
|
|
|
|
"Main" -> ActorRole.Main
|
|
|
|
"Supporting" -> ActorRole.Supporting
|
|
|
|
"Background" -> ActorRole.Background
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
val name = it.name?.full ?: it.name?.native ?: ""
|
|
|
|
val image = it.image?.large ?: it.image?.medium
|
|
|
|
Pair(Actor(name, image), role)
|
|
|
|
}
|
|
|
|
|
2023-07-02 16:25:42 +00:00
|
|
|
return newAnimeLoadResponse(title ?: "", url, TvType.Anime) {
|
2023-06-30 05:51:14 +00:00
|
|
|
engName = showData.altNames?.firstOrNull()
|
2023-07-13 06:27:01 +00:00
|
|
|
posterUrl = trackers?.coverImage?.extraLarge ?: trackers?.coverImage?.large ?: poster
|
|
|
|
backgroundPosterUrl = trackers?.bannerImage ?: showData.banner
|
2023-06-29 13:47:08 +00:00
|
|
|
rating = showData.averageScore?.times(100)
|
|
|
|
tags = showData.genres
|
|
|
|
year = showData.airedStart?.year
|
|
|
|
duration = showData.episodeDuration?.div(60_000)
|
|
|
|
addTrailer(showData.prevideos.filter { it.isNotBlank() }
|
|
|
|
.map { "https://www.youtube.com/watch?v=$it" })
|
|
|
|
addEpisodes(DubStatus.Subbed, episodes.first)
|
|
|
|
addEpisodes(DubStatus.Dubbed, episodes.second)
|
|
|
|
addActors(characters)
|
|
|
|
//this.recommendations = recommendations
|
|
|
|
|
|
|
|
showStatus = getStatus(showData.status.toString())
|
2023-07-13 06:27:01 +00:00
|
|
|
addMalId(trackers?.idMal)
|
|
|
|
addAniListId(trackers?.id)
|
2023-06-29 13:47:08 +00:00
|
|
|
plot = description?.replace(Regex("""<(.*?)>"""), "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun loadLinks(
|
|
|
|
data: String,
|
|
|
|
isCasting: Boolean,
|
|
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
|
|
callback: (ExtractorLink) -> Unit
|
|
|
|
): Boolean {
|
|
|
|
|
|
|
|
val loadData = parseJson<AnichiLoadData>(data)
|
|
|
|
|
2024-01-26 12:03:51 +00:00
|
|
|
invokeInternalSources(
|
|
|
|
loadData.hash,
|
|
|
|
loadData.dubStatus,
|
|
|
|
loadData.episode,
|
|
|
|
subtitleCallback,
|
|
|
|
callback
|
2023-06-29 13:47:08 +00:00
|
|
|
)
|
2023-07-13 06:27:01 +00:00
|
|
|
|
2023-07-13 16:34:46 +00:00
|
|
|
return true
|
2023-07-13 06:27:01 +00:00
|
|
|
}
|
|
|
|
|
2023-06-29 13:47:08 +00:00
|
|
|
companion object {
|
2023-07-13 16:34:46 +00:00
|
|
|
const val apiUrl = BuildConfig.ANICHI_API
|
|
|
|
const val serverUrl = BuildConfig.ANICHI_SERVER
|
|
|
|
const val apiEndPoint = BuildConfig.ANICHI_ENDPOINT
|
|
|
|
|
2023-08-12 17:42:08 +00:00
|
|
|
const val anilistApi = "https://graphql.anilist.co"
|
|
|
|
const val jikanApi = "https://api.jikan.moe/v4"
|
2023-06-29 13:47:08 +00:00
|
|
|
|
|
|
|
private const val mainHash = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406"
|
|
|
|
private const val popularHash = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c"
|
2023-07-02 16:05:21 +00:00
|
|
|
private const val slugHash = "bf603205eb2533ca21d0324a11f623854d62ed838a27e1b3fcfb712ab98b03f4"
|
2023-06-29 13:47:08 +00:00
|
|
|
private const val detailHash = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b"
|
2023-07-13 16:34:46 +00:00
|
|
|
const val serverHash = "5e7e17cdd0166af5a2d8f43133d9ce3ce9253d1fdb5160a0cfd515564f98d061"
|
2023-06-29 13:47:08 +00:00
|
|
|
|
2023-07-13 16:34:46 +00:00
|
|
|
val headers = mapOf(
|
2023-06-29 13:47:08 +00:00
|
|
|
"app-version" to "android_c-247",
|
|
|
|
"from-app" to BuildConfig.ANICHI_APP,
|
|
|
|
"platformstr" to "android_c",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|