Skip to content

Commit 1699664

Browse files
test
1 parent 4120fc3 commit 1699664

3 files changed

Lines changed: 347 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// use an integer for version numbers
2+
version = 1
3+
4+
cloudstream {
5+
// All of these properties are optional, you can safely remove them
6+
7+
description = "Nonton anime subtitle Indonesia dari animeinweb.com"
8+
authors = listOf("CloudStream")
9+
10+
/**
11+
* Status int as the following:
12+
* 0: Down
13+
* 1: Ok
14+
* 2: Slow
15+
* 3: Beta only
16+
* */
17+
status = 1 // will be 3 if unspecified
18+
tvTypes = listOf(
19+
"AnimeMovie",
20+
"Anime",
21+
"OVA",
22+
)
23+
24+
iconUrl = "https://animeinweb.com/wp-content/uploads/2021/10/cropped-animeinweb-32x32.png"
25+
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package com.animeinweb
2+
3+
import com.lagradost.cloudstream3.*
4+
import com.lagradost.cloudstream3.utils.*
5+
6+
class AnimeInWebProvider : MainAPI() {
7+
override var mainUrl = "https://animeinweb.com"
8+
override var name = "AnimeInWeb"
9+
override val hasMainPage = true
10+
override var lang = "id"
11+
override val hasDownloadSupport = true
12+
override val supportedTypes = setOf(
13+
TvType.Anime,
14+
TvType.AnimeMovie,
15+
TvType.OVA,
16+
)
17+
18+
// ─── API ──────────────────────────────────────────────────────────────────
19+
private val apiBase = "https://xyz-api.animein.net/3/2"
20+
private val cdnBase = "https://xyz-api.animein.net"
21+
private val idUser = "388448"
22+
private val keyClient = "WOcU74yO3OznevT4fju3CS2ovlff9vVFt788u6tZNUjxyrWlqQ"
23+
private val apkVer = "5.0.2"
24+
25+
private val defaultHeaders = mapOf(
26+
"Accept" to "application/json",
27+
"User-Agent" to "AnimeInWeb/$apkVer Android",
28+
)
29+
private val baseParams get() = mapOf(
30+
"id_user" to idUser,
31+
"key_client" to keyClient,
32+
"apk_ver" to apkVer,
33+
)
34+
35+
private suspend fun apiGet(path: String, extra: Map<String, String> = emptyMap()): NiceResponse {
36+
val params = (baseParams + extra).entries.joinToString("&") { "${it.key}=${it.value}" }
37+
return app.get("$apiBase/$path?$params", referer = mainUrl, headers = defaultHeaders)
38+
}
39+
40+
/** Fix relative image path → absolute URL */
41+
private fun String?.fixImage(): String? {
42+
if (this.isNullOrEmpty()) return null
43+
return if (startsWith("http")) this else "$cdnBase$this"
44+
}
45+
46+
// ─── Type / Status ────────────────────────────────────────────────────────
47+
companion object {
48+
fun getType(t: String): TvType = when {
49+
t.equals("OVA", ignoreCase = true) -> TvType.OVA
50+
t.equals("MOVIE", ignoreCase = true) -> TvType.AnimeMovie
51+
else -> TvType.Anime
52+
}
53+
fun getStatus(s: String): ShowStatus = when {
54+
s.equals("ONGOING", ignoreCase = true) -> ShowStatus.Ongoing
55+
s.equals("FINISHED", ignoreCase = true) -> ShowStatus.Completed
56+
else -> ShowStatus.Completed
57+
}
58+
}
59+
60+
// ─── Main Page ────────────────────────────────────────────────────────────
61+
override val mainPage = mainPageOf(
62+
"home/hot" to "🔥 Populer",
63+
"home/update" to "🆕 Terbaru Update",
64+
"home/ongoing" to "📺 Sedang Tayang",
65+
"home/finished" to "✅ Selesai",
66+
"home/movie" to "🎬 Anime Movie",
67+
"home/recommended" to "⭐ Rekomendasi",
68+
)
69+
70+
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
71+
val resp = apiGet(request.data, extra = mapOf("limit" to "24", "page" to "$page", "genre_in" to ""))
72+
val items = resp.parsedSafe<ApiListResponse>()?.data?.allItems() ?: emptyList()
73+
return newHomePageResponse(request.name, items.map { it.toSearchResponse() })
74+
}
75+
76+
// ─── Search ───────────────────────────────────────────────────────────────
77+
override suspend fun search(query: String): List<SearchResponse> {
78+
val resp = apiGet("search", extra = mapOf("q" to query, "limit" to "30"))
79+
return resp.parsedSafe<ApiListResponse>()?.data?.allItems()
80+
?.map { it.toSearchResponse() } ?: emptyList()
81+
}
82+
83+
// ─── Load Detail ──────────────────────────────────────────────────────────
84+
// Endpoint: GET /3/2/movie/detail/{id}
85+
// Response: { data: { movie, episode (latest), season[] } }
86+
override suspend fun load(url: String): LoadResponse? {
87+
val movieId = url.substringAfterLast("/")
88+
89+
val detail = apiGet("movie/detail/$movieId")
90+
.parsedSafe<ApiDetailResponse>()?.data ?: return null
91+
val movie = detail.movie ?: return null
92+
val seasons = detail.season ?: emptyList()
93+
94+
val episodes = buildEpisodeList(movieId)
95+
96+
val tags = movie.genre
97+
?.split(",")
98+
?.map { it.trim() }
99+
?.filter { it.isNotEmpty() && it != "-" }
100+
101+
return newAnimeLoadResponse(
102+
name = movie.title ?: "Unknown",
103+
url = url,
104+
type = getType(movie.type ?: "SERIES")
105+
) {
106+
posterUrl = movie.image_poster.fixImage()
107+
backgroundPosterUrl = movie.image_cover.fixImage()
108+
plot = movie.synopsis?.replace("\r\n", "\n")
109+
this.tags = tags
110+
showStatus = getStatus(movie.status ?: "")
111+
year = movie.year?.toIntOrNull()
112+
duration = movie.duration?.toIntOrNull()
113+
114+
movie.studio?.takeIf { it.isNotEmpty() }?.let {
115+
this.actors = listOf(ActorData(Actor(it)))
116+
}
117+
118+
if (seasons.size > 1) {
119+
this.recommendations = seasons
120+
.filter { it.id != movieId }
121+
.map { it.toSearchResponse() }
122+
}
123+
124+
addEpisodes(DubStatus.Subbed, episodes)
125+
}
126+
}
127+
128+
private suspend fun buildEpisodeList(movieId: String): List<Episode> {
129+
val resp = apiGet(
130+
"movie/$movieId/episode",
131+
extra = mapOf("limit" to "999", "sort" to "asc")
132+
)
133+
val list = resp.parsedSafe<ApiEpisodeListResponse>()?.data?.allItems()
134+
?: return emptyList()
135+
136+
return list.mapIndexed { idx, ep ->
137+
val epNum = ep.index?.toIntOrNull() ?: ep.number ?: (idx + 1)
138+
// data = episodeId (that's all we need for streamnew)
139+
newEpisode(ep.id ?: "") {
140+
this.name = ep.title?.takeIf { it.isNotBlank() } ?: "Episode $epNum"
141+
this.episode = epNum
142+
this.posterUrl = ep.image.fixImage()
143+
this.addDate(ep.key_time ?: ep.aired)
144+
}
145+
}
146+
}
147+
148+
// ─── Load Links ───────────────────────────────────────────────────────────
149+
// Confirmed endpoint: GET /3/2/episode/streamnew/{episodeId}
150+
// Response: { data: { episode, episode_next, server: [ {link, quality, type, name} ] } }
151+
override suspend fun loadLinks(
152+
data: String, // = episodeId e.g. "313448"
153+
isCasting: Boolean,
154+
subtitleCallback: (SubtitleFile) -> Unit,
155+
callback: (ExtractorLink) -> Unit
156+
): Boolean {
157+
val episodeId = data.ifEmpty { return false }
158+
159+
val resp = apiGet("episode/streamnew/$episodeId")
160+
val servers = resp.parsedSafe<ApiStreamResponse>()?.data?.server
161+
?: return false
162+
163+
servers.forEach { server ->
164+
val link = server.link?.ifEmpty { null } ?: return@forEach
165+
val quality = server.quality ?: ""
166+
val label = buildString {
167+
append(server.name ?: "Server")
168+
if (quality.isNotEmpty()) append(" $quality")
169+
}
170+
171+
when (server.type?.lowercase()) {
172+
"direct" -> {
173+
// Direct MP4 from storages.animein.net — confirmed
174+
callback(
175+
newExtractorLink(
176+
source = name,
177+
name = label,
178+
url = link,
179+
type = ExtractorLinkType.VIDEO,
180+
) {
181+
this.quality = when (quality) {
182+
"1080p" -> Qualities.P1080.value
183+
"720p" -> Qualities.P720.value
184+
"480p" -> Qualities.P480.value
185+
"360p" -> Qualities.P360.value
186+
else -> Qualities.Unknown.value
187+
}
188+
this.isM3u8 = false
189+
}
190+
)
191+
}
192+
else -> {
193+
// Embed / iframe server
194+
loadExtractor(link, mainUrl, subtitleCallback, callback)
195+
}
196+
}
197+
}
198+
199+
return true
200+
}
201+
}
202+
203+
// ─── Data Classes ─────────────────────────────────────────────────────────────
204+
205+
// LIST (home/search)
206+
data class ApiListResponse(
207+
val status: Int? = null,
208+
val error: Boolean? = null,
209+
val data: ListData? = null,
210+
)
211+
data class ListData(
212+
val movie: List<AnimeItem>? = null,
213+
val anime: List<AnimeItem>? = null,
214+
val list: List<AnimeItem>? = null,
215+
) { fun allItems() = movie ?: anime ?: list ?: emptyList() }
216+
217+
data class AnimeItem(
218+
val id: String? = null,
219+
val title: String? = null,
220+
val synopsis: String? = null,
221+
val synonyms: String? = null,
222+
val image_poster: String? = null,
223+
val image_cover: String? = null,
224+
val type: String? = null,
225+
val year: String? = null,
226+
val day: String? = null,
227+
val status: String? = null,
228+
val views: String? = null,
229+
val favorites: String? = null,
230+
val studio: String? = null,
231+
val aired_start: String? = null,
232+
val aired_end: String? = null,
233+
val genre: String? = null,
234+
val score: String? = null,
235+
val duration: String? = null,
236+
val season: String? = null,
237+
val is_fav: Any? = null,
238+
val total_episode: Int? = null,
239+
val latest_episode: Int? = null,
240+
) {
241+
fun toSearchResponse() = newAnimeSearchResponse(
242+
name = title ?: "Unknown",
243+
url = "https://animeinweb.com/anime/$id",
244+
type = AnimeInWebProvider.getType(type ?: "SERIES")
245+
) {
246+
posterUrl = if (image_poster?.startsWith("http") == true) image_poster
247+
else "https://xyz-api.animein.net$image_poster"
248+
addSub(latest_episode ?: total_episode)
249+
}
250+
}
251+
252+
// DETAIL → { data: { movie, episode, season[] } }
253+
data class ApiDetailResponse(
254+
val status: Int? = null,
255+
val error: Boolean? = null,
256+
val data: DetailData? = null,
257+
)
258+
data class DetailData(
259+
val movie: AnimeItem? = null,
260+
val episode: EpisodeItem? = null,
261+
val season: List<AnimeItem>? = null,
262+
)
263+
264+
// EPISODE LIST → { data: { episode[] } }
265+
data class ApiEpisodeListResponse(
266+
val status: Int? = null,
267+
val data: EpisodeListData? = null,
268+
)
269+
data class EpisodeListData(
270+
val episode: List<EpisodeItem>? = null,
271+
val list: List<EpisodeItem>? = null,
272+
) { fun allItems() = episode ?: list ?: emptyList() }
273+
274+
data class EpisodeItem(
275+
val id: String? = null,
276+
val title: String? = null,
277+
val index: String? = null, // "1", "2", ...
278+
val number: Int? = null,
279+
val episode_number: Int? = null,
280+
val views: String? = null,
281+
val id_movie: String? = null,
282+
val key_time: String? = null, // "12 Jan 2026"
283+
val aired: String? = null,
284+
val image: String? = null, // may be relative
285+
val thumbnail: String? = null,
286+
)
287+
288+
// STREAM → GET /episode/streamnew/{id}
289+
// { data: { episode, episode_next, server: [{id, link, quality, type, name, ...}] } }
290+
data class ApiStreamResponse(
291+
val status: Int? = null,
292+
val error: Boolean? = null,
293+
val data: StreamData? = null,
294+
)
295+
data class StreamData(
296+
val episode: EpisodeItem? = null,
297+
val episode_next: EpisodeItem? = null,
298+
val server: List<StreamServer>? = null,
299+
)
300+
data class StreamServer(
301+
val id: String? = null,
302+
val link: String? = null, // confirmed: direct MP4 URL
303+
val quality: String? = null, // "360p" | "480p" | "720p" | "1080p"
304+
val key_file_size: String? = null,
305+
val name: String? = null, // e.g. "RAPSODI"
306+
val type: String? = null, // "direct" | "embed"
307+
val domain: String? = null,
308+
val username: String? = null,
309+
val server_id: String? = null,
310+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.animeinweb
2+
3+
import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
4+
import com.lagradost.cloudstream3.plugins.Plugin
5+
import android.content.Context
6+
7+
@CloudstreamPlugin
8+
class AnimeInWebProviderPlugin : Plugin() {
9+
override fun load(context: Context) {
10+
registerMainAPI(AnimeInWebProvider())
11+
}
12+
}

0 commit comments

Comments
 (0)