@@ -2,6 +2,7 @@ package com.animeinweb
22
33import com.lagradost.cloudstream3.*
44import com.lagradost.cloudstream3.utils.*
5+ import com.lagradost.cloudstream3.utils.AppUtils.parseJson
56
67class AnimeInWebProvider : MainAPI () {
78 override var mainUrl = " https://animeinweb.com"
@@ -26,24 +27,30 @@ class AnimeInWebProvider : MainAPI() {
2627 " Accept" to " application/json" ,
2728 " User-Agent" to " AnimeInWeb/$apkVer Android" ,
2829 )
29- private val baseParams get() = mapOf (
30- " id_user" to idUser,
31- " key_client" to keyClient,
32- " apk_ver" to apkVer,
33- )
3430
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)
31+ private fun buildParams (extra : Map <String , String > = emptyMap()): String {
32+ val base = mapOf (
33+ " id_user" to idUser,
34+ " key_client" to keyClient,
35+ " apk_ver" to apkVer,
36+ )
37+ return (base + extra).entries.joinToString(" &" ) { " ${it.key} =${it.value} " }
38+ }
39+
40+ private suspend fun apiGet (path : String , extra : Map <String , String > = emptyMap()): String {
41+ return app.get(
42+ " $apiBase /$path ?${buildParams(extra)} " ,
43+ referer = mainUrl,
44+ headers = defaultHeaders,
45+ ).text
3846 }
3947
40- /* * Fix relative image path → absolute URL */
4148 private fun String?.fixImage (): String? {
4249 if (this .isNullOrEmpty()) return null
4350 return if (startsWith(" http" )) this else " $cdnBase$this "
4451 }
4552
46- // ─── Type / Status ────────────────────────────────────────────────────────
53+ // ─── Helpers ────── ────────────────────────────────────────────────────────
4754 companion object {
4855 fun getType (t : String ): TvType = when {
4956 t.equals(" OVA" , ignoreCase = true ) -> TvType .OVA
@@ -55,6 +62,13 @@ class AnimeInWebProvider : MainAPI() {
5562 s.equals(" FINISHED" , ignoreCase = true ) -> ShowStatus .Completed
5663 else -> ShowStatus .Completed
5764 }
65+ fun parseQuality (q : String ): Int = when {
66+ q.contains(" 1080" ) -> Qualities .P1080 .value
67+ q.contains(" 720" ) -> Qualities .P720 .value
68+ q.contains(" 480" ) -> Qualities .P480 .value
69+ q.contains(" 360" ) -> Qualities .P360 .value
70+ else -> Qualities .Unknown .value
71+ }
5872 }
5973
6074 // ─── Main Page ────────────────────────────────────────────────────────────
@@ -68,28 +82,26 @@ class AnimeInWebProvider : MainAPI() {
6882 )
6983
7084 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()
85+ val json = apiGet(request.data, extra = mapOf (" limit" to " 24" , " page" to " $page " , " genre_in" to " " ))
86+ val items = parseJson <ApiListResponse >(json) .data?.allItems() ? : emptyList()
7387 return newHomePageResponse(request.name, items.map { it.toSearchResponse() })
7488 }
7589
7690 // ─── Search ───────────────────────────────────────────────────────────────
7791 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()
92+ val json = apiGet(" search" , extra = mapOf (" q" to query, " limit" to " 30" ))
93+ return parseJson <ApiListResponse >(json) .data?.allItems()
8094 ?.map { it.toSearchResponse() } ? : emptyList()
8195 }
8296
8397 // ─── Load Detail ──────────────────────────────────────────────────────────
8498 // Endpoint: GET /3/2/movie/detail/{id}
85- // Response: { data: { movie, episode (latest), season[] } }
8699 override suspend fun load (url : String ): LoadResponse ? {
87100 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()
101+ val json = apiGet(" movie/detail/$movieId " )
102+ val detail = parseJson<ApiDetailResponse >(json).data ? : return null
103+ val movie = detail.movie ? : return null
104+ val seasons = detail.season ? : emptyList()
93105
94106 val episodes = buildEpisodeList(movieId)
95107
@@ -111,33 +123,25 @@ class AnimeInWebProvider : MainAPI() {
111123 year = movie.year?.toIntOrNull()
112124 duration = movie.duration?.toIntOrNull()
113125
114- movie.studio?.takeIf { it.isNotEmpty() }?.let {
115- this .actors = listOf (ActorData (Actor (it)))
116- }
117-
118126 if (seasons.size > 1 ) {
119127 this .recommendations = seasons
120- .filter { it .id != movieId }
121- .map { it .toSearchResponse() }
128+ .filter { season -> season .id != movieId }
129+ .map { season -> season .toSearchResponse() }
122130 }
123131
124132 addEpisodes(DubStatus .Subbed , episodes)
125133 }
126134 }
127135
128136 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()
137+ val json = apiGet(" movie/$movieId /episode" , extra = mapOf (" limit" to " 999" , " sort" to " asc" ))
138+ val list = parseJson<ApiEpisodeListResponse >(json).data?.allItems() ? : return emptyList()
135139
136140 return list.mapIndexed { idx, ep ->
137141 val epNum = ep.index?.toIntOrNull() ? : ep.number ? : (idx + 1 )
138- // data = episodeId (that's all we need for streamnew)
142+ val epTitle = if ( ! ep.title.isNullOrBlank()) ep.title else " Episode $epNum "
139143 newEpisode(ep.id ? : " " ) {
140- this .name = ep.title?. takeIf { it.isNotBlank() } ? : " Episode $epNum "
144+ this .name = epTitle
141145 this .episode = epNum
142146 this .posterUrl = ep.image.fixImage()
143147 this .addDate(ep.key_time ? : ep.aired)
@@ -147,50 +151,39 @@ class AnimeInWebProvider : MainAPI() {
147151
148152 // ─── Load Links ───────────────────────────────────────────────────────────
149153 // Confirmed endpoint: GET /3/2/episode/streamnew/{episodeId}
150- // Response: { data: { episode, episode_next, server: [ {link, quality, type, name} ] } }
154+ // server[].link = direct MP4 on storages.animein.net
155+ // server[].quality = "360p" | "480p" | "720p" | "1080p"
156+ // server[].type = "direct" | "embed"
151157 override suspend fun loadLinks (
152- data : String , // = episodeId e.g. "313448"
158+ data : String , // episodeId e.g. "313448"
153159 isCasting : Boolean ,
154160 subtitleCallback : (SubtitleFile ) -> Unit ,
155161 callback : (ExtractorLink ) -> Unit
156162 ): Boolean {
157- val episodeId = data.ifEmpty { return false }
163+ if ( data.isEmpty()) return false
158164
159- val resp = apiGet(" episode/streamnew/$episodeId " )
160- val servers = resp.parsedSafe<ApiStreamResponse >()?.data?.server
161- ? : return false
165+ val json = apiGet(" episode/streamnew/$data " )
166+ val servers = parseJson<ApiStreamResponse >(json).data?.server ? : return false
162167
163168 servers.forEach { server ->
164- val link = server.link?.ifEmpty { null } ? : return @forEach
169+ val link = server.link ? : return @forEach
165170 val quality = server.quality ? : " "
166- val label = buildString {
167- append(server.name ? : " Server" )
168- if (quality.isNotEmpty()) append(" $quality " )
169- }
171+ val label = " ${server.name ? : " Server" } $quality " .trim()
170172
171173 when (server.type?.lowercase()) {
172174 " direct" -> {
173- // Direct MP4 from storages.animein.net — confirmed
174175 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- }
176+ ExtractorLink (
177+ source = name,
178+ name = label,
179+ url = link,
180+ referer = mainUrl,
181+ quality = parseQuality(quality),
182+ isM3u8 = false ,
183+ )
190184 )
191185 }
192186 else -> {
193- // Embed / iframe server
194187 loadExtractor(link, mainUrl, subtitleCallback, callback)
195188 }
196189 }
@@ -202,7 +195,6 @@ class AnimeInWebProvider : MainAPI() {
202195
203196// ─── Data Classes ─────────────────────────────────────────────────────────────
204197
205- // LIST (home/search)
206198data class ApiListResponse (
207199 val status : Int? = null ,
208200 val error : Boolean? = null ,
@@ -238,18 +230,20 @@ data class AnimeItem(
238230 val total_episode : Int? = null ,
239231 val latest_episode : Int? = null ,
240232) {
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)
233+ fun toSearchResponse (): AnimeSearchResponse {
234+ val poster = if (image_poster?.startsWith(" http" ) == true ) image_poster
235+ else " https://xyz-api.animein.net$image_poster "
236+ return newAnimeSearchResponse(
237+ name = title ? : " Unknown" ,
238+ url = " https://animeinweb.com/anime/$id " ,
239+ type = AnimeInWebProvider .getType(type ? : " SERIES" ),
240+ ) {
241+ posterUrl = poster
242+ addSub(latest_episode ? : total_episode)
243+ }
249244 }
250245}
251246
252- // DETAIL → { data: { movie, episode, season[] } }
253247data class ApiDetailResponse (
254248 val status : Int? = null ,
255249 val error : Boolean? = null ,
@@ -261,7 +255,6 @@ data class DetailData(
261255 val season : List <AnimeItem >? = null ,
262256)
263257
264- // EPISODE LIST → { data: { episode[] } }
265258data class ApiEpisodeListResponse (
266259 val status : Int? = null ,
267260 val data : EpisodeListData ? = null ,
@@ -274,37 +267,32 @@ data class EpisodeListData(
274267data class EpisodeItem (
275268 val id : String? = null ,
276269 val title : String? = null ,
277- val index : String? = null , // "1", "2", ...
270+ val index : String? = null ,
278271 val number : Int? = null ,
279- val episode_number : Int? = null ,
280272 val views : String? = null ,
281273 val id_movie : String? = null ,
282- val key_time : String? = null , // "12 Jan 2026"
274+ val key_time : String? = null ,
283275 val aired : String? = null ,
284- val image : String? = null , // may be relative
285- val thumbnail : String? = null ,
276+ val image : String? = null ,
286277)
287278
288- // STREAM → GET /episode/streamnew/{id}
289- // { data: { episode, episode_next, server: [{id, link, quality, type, name, ...}] } }
290279data class ApiStreamResponse (
291280 val status : Int? = null ,
292281 val error : Boolean? = null ,
293282 val data : StreamData ? = null ,
294283)
295284data class StreamData (
296- val episode : EpisodeItem ? = null ,
297- val episode_next : EpisodeItem ? = null ,
285+ val episode : EpisodeItem ? = null ,
286+ val episode_next : EpisodeItem ? = null ,
298287 val server : List <StreamServer >? = null ,
299288)
300289data 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 ,
290+ val id : String? = null ,
291+ val link : String? = null ,
292+ val quality : String? = null ,
293+ val name : String? = null ,
294+ val type : String? = null ,
295+ val domain : String? = null ,
296+ val username : String? = null ,
297+ val server_id : String? = null ,
310298)
0 commit comments