Describe your suggested feature
package com.horis.cncverse
import android.content.Context
import com.horis.cncverse.entities.EpisodesData
import com.horis.cncverse.entities.PostData
import com.horis.cncverse.entities.SearchData
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.nodes.Element
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.api.Log
import java.net.URLEncoder
import com.lagradost.nicehttp.NiceResponse
class NetflixMirrorProvider : MainAPI() {
companion object {
var context: Context? = null
}
override val supportedTypes = setOf(TvType.Movie, TvType.TvSeries)
override var lang = "hi"
override var mainUrl = "https://net52.cc"
private val newUrl = "https://net52.cc"
override var name = "Netflix"
override val hasMainPage = true
private var cookie_value = ""
private val headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language" to "en-IN,en-US;q=0.9,en;q=0.8",
"Cache-Control" to "max-age=0",
"Connection" to "keep-alive",
"sec-ch-ua" to ""Not(A:Brand";v="8", "Chromium";v="144", "Android WebView";v="144"",
"sec-ch-ua-mobile" to "?0",
"sec-ch-ua-platform" to ""Android"",
"Sec-Fetch-Dest" to "document",
"Sec-Fetch-Mode" to "navigate",
"Sec-Fetch-Site" to "same-origin",
"Sec-Fetch-User" to "?1",
"Upgrade-Insecure-Requests" to "1",
"User-Agent" to "Mozilla/5.0 (Linux; Android 13; Pixel 5 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Safari/537.36 /OS.Gatu v3.0",
"X-Requested-With" to "XMLHttpRequest"
)
// Native playlist flow URLs
private val playUrl = "https://net77.cc/play.php"
private val playlistBaseUrl = "https://net52.cc/playlist.php"
private val nativeReferer = "https://net77.cc/home"
private val nativeOrigin = "https://net77.cc"
// Cookie store for native flow
private var nativeCookies = mutableMapOf<String, String>()
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse? {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val cookies = mapOf("t_hash_t" to cookie_value, "ott" to "nf", "hd" to "on")
val document = app.get(
"$mainUrl/mobile/home?app=1",
cookies = cookies,
headers = headers,
referer = "$mainUrl/mobile/home?app=1"
).document
val items = document.select(".tray-container, #top10").map {
it.toHomePageList()
}
return newHomePageResponse(items, false)
}
private fun Element.toHomePageList(): HomePageList {
val name = select("h2, span").text()
val items = select("article, .top10-post").mapNotNull {
it.toSearchResult()
}
return HomePageList(name, items, isHorizontalImages = false)
}
private fun Element.toSearchResult(): SearchResponse? {
val id = selectFirst("a")?.attr("data-post") ?: attr("data-post")
if (id.isNullOrBlank()) return null
return newAnimeSearchResponse("", Id(id).toJson()) {
posterUrl = "https://imgcdn.kim/poster/v/$id.jpg"
posterHeaders = mapOf("Referer" to "$mainUrl/home")
}
}
override suspend fun search(query: String): List {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
val data = app.get(
"$mainUrl/mobile/search.php?s=$query&t=$unixTime",
referer = "$mainUrl/home",
cookies = cookies
).parsed()
return data.searchResult.map {
newAnimeSearchResponse(it.t, Id(it.id).toJson()) {
posterUrl = "https://imgcdn.kim/poster/v/${it.id}.jpg"
posterHeaders = mapOf("Referer" to "$mainUrl/home")
}
}
}
override suspend fun load(url: String): LoadResponse? {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val id = parseJson(url).id
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
val data = app.get(
"$mainUrl/mobile/post.php?id=$id&t=$unixTime",
headers,
referer = "$mainUrl/home",
cookies = cookies
).parsed()
val title = data.title
val episodes = arrayListOf<Episode>()
val isMovie = data.episodes.isEmpty() || data.episodes.first() == null
val tmdbId = data.tmdb_id ?: resolveTmdbId(title, data.year, isMovie)
if (isMovie) {
episodes.add(newEpisode(LoadData(title, id, tmdbId)) {
name = title
})
} else {
data.episodes.filterNotNull().mapTo(episodes) {
newEpisode(LoadData(
title, it.id, tmdbId,
it.s.replace("S", "").toIntOrNull(),
it.ep.replace("E", "").toIntOrNull()
)) {
this.name = it.t
this.episode = it.ep.replace("E", "").toIntOrNull()
this.season = it.s.replace("S", "").toIntOrNull()
this.posterUrl = "https://imgcdn.kim/poster/v/150/${it.id}.jpg"
this.runTime = it.time.replace("m", "").toIntOrNull()
}
}
if (data.nextPageShow == 1) {
episodes.addAll(getEpisodes(title, url, data.nextPageSeason!!, 2, tmdbId))
}
data.season?.dropLast(1)?.amap {
episodes.addAll(getEpisodes(title, url, it.id, 1, tmdbId))
}
}
val cast = data.cast?.split(",")?.map {
it.trim()
}
?.filter {
it.isNotEmpty()
}?.map {
ActorData(Actor(it))
}
val genre = data.genre?.split(",")?.map {
it.trim()
}?.filter {
it.isNotEmpty()
}
val type = if (isMovie) TvType.Movie else TvType.TvSeries
return newTvSeriesLoadResponse(title, url, type, episodes) {
posterUrl = "https://imgcdn.kim/poster/v/$id.jpg"
backgroundPosterUrl = "https://imgcdn.kim/poster/v/$id.jpg"
posterHeaders = mapOf("Referer" to "$mainUrl/home")
plot = data.desc
year = data.year.toIntOrNull()
tags = genre
actors = cast
this.score = Score.from10(data.match?.replace("IMDb ", ""))
this.duration = convertRuntimeToMinutes(data.runtime.toString())
this.contentRating = data.ua
}
}
private suspend fun getEpisodes(
title: String, eid: String, sid: String, page: Int, tmdbId: String?
): List {
val episodes = arrayListOf()
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
var pg = page
while (true) {
val data = app.get(
"$mainUrl/mobile/episodes.php?s=$sid&series=$eid&t=$unixTime&page=$pg",
headers,
referer = "$mainUrl/home",
cookies = cookies
).parsed()
data.episodes?.mapTo(episodes) {
newEpisode(LoadData(
title, it.id, tmdbId,
it.s.replace("S", "").toIntOrNull(),
it.ep.replace("E", "").toIntOrNull()
)) {
name = it.t
episode = it.ep.replace("E", "").toIntOrNull()
season = it.s.replace("S", "").toIntOrNull()
this.posterUrl = "https://imgcdn.kim/poster/v/150/${it.id}.jpg"
this.runTime = it.time.replace("m", "").toIntOrNull()
}
}
if (data.nextPageShow == 0) break
pg++
}
return episodes
}
// Native play.php -> playlist.php flow with subtitles
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val loadData = parseJson(data)
val id = loadData.id
// NewTV API flow (same as HotStar/Disney/Prime providers) — primary.
// No Cloudflare cookie wall: resolves the API base via mobiledetect.*
// domains -> checknewtv.php -> token_hash, then asks for the HLS link.
try {
NetmirrorThrottler.throttle()
val apiBase = resolveApiUrl()
val newTvResp = app.get(
"$apiBase/newtv/player.php?id=$id",
headers = buildNewTvHeaders("nf")
).parsed<NewTvPlayerResponse>()
if (!newTvResp.video_link.isNullOrBlank()) {
Log.i("NetflixMirror", "NewTV flow ok for id=$id")
callback.invoke(
newExtractorLink(name, name, newTvResp.video_link, type = ExtractorLinkType.M3U8) {
this.referer = newTvResp.referer ?: apiBase
}
)
return true
}
Log.w("NetflixMirror", "NewTV flow empty for id=$id, falling back")
} catch (e: Exception) {
Log.w("NetflixMirror", "NewTV flow failed for id=$id: ${e.message}")
}
// Ensure we have fresh cookies for the native flow
ensureNativeCookies(id)
val playResp = try {
NetmirrorThrottler.throttle()
app.post(
playUrl,
data = mapOf("id" to id),
headers = mapOf(
"Accept" to "application/json, text/javascript, */*; q=0.01",
"Accept-Language" to "en-IN,en-US;q=0.9,en;q=0.8",
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
"Origin" to nativeOrigin,
"Referer" to nativeReferer,
"X-Requested-With" to "XMLHttpRequest",
"User-Agent" to headers["User-Agent"]!!
),
cookies = nativeCookies
).parsed<PlayResponse>()
} catch (e: Exception) {
Log.e("NetflixMirror", "play.php failed for id=$id: ${e.message}")
// Fallback to old TMDB embed method
return loadLinksFallback(loadData, subtitleCallback, callback)
}
val h = playResp.h ?: run {
Log.e("NetflixMirror", "play.php returned no h-token for id=$id")
return loadLinksFallback(loadData, subtitleCallback, callback)
}
val tm = unixTime
val playlistUrl = buildString {
append(playlistBaseUrl)
append("?id=$id")
append("&t=${URLEncoder.encode(loadData.title, "UTF-8")}")
append("&tm=$tm")
append("&h=${URLEncoder.encode(h, "UTF-8")}")
}
val playlist = try {
NetmirrorThrottler.throttle()
val playlistText = app.get(
playlistUrl,
headers = mapOf(
"Accept" to "application/json, text/javascript, */*; q=0.01",
"Accept-Language" to "en-IN,en-US;q=0.9,en;q=0.8",
"Referer" to nativeReferer,
"Origin" to nativeOrigin,
"X-Requested-With" to "XMLHttpRequest",
"User-Agent" to headers["User-Agent"]!!
),
cookies = nativeCookies
).text.trim()
// Server sometimes wraps the playlist in a JSON array ([{...}]) and
// sometimes returns the object directly — handle both.
if (playlistText.startsWith("[")) {
parseJson<List<PlaylistResponse>>(playlistText).firstOrNull()
} else {
parseJson<PlaylistResponse>(playlistText)
}
} catch (e: Exception) {
Log.e("NetflixMirror", "playlist.php failed for id=$id: ${e.message}")
return loadLinksFallback(loadData, subtitleCallback, callback)
}
if (playlist == null) {
Log.e("NetflixMirror", "playlist.php returned empty for id=$id")
return loadLinksFallback(loadData, subtitleCallback, callback)
}
var found = false
// Step 3: Register HLS sources with quality
playlist.sources?.forEach {
source ->
val streamUrl = if (source.file.startsWith("http")) {
source.file
} else {
"https://net52.cc${source.file}"
}
val quality = when {
source.label.contains("1080", true) -> Qualities.P1080.value
source.label.contains("720", true) -> Qualities.P720.value
source.label.contains("480", true) -> Qualities.P480.value
source.label.contains("360", true) -> Qualities.P360.value
source.label.contains("Full HD", true) -> Qualities.P1080.value
source.label.contains("Mid HD", true) -> Qualities.P720.value
source.label.contains("Low HD", true) -> Qualities.P480.value
else -> Qualities.Unknown.value
}
callback.invoke(
newExtractorLink(
name,
"$name ${source.label}",
streamUrl,
type = ExtractorLinkType.M3U8
) {
this.referer = nativeReferer
this.quality = quality
}
)
found = true
}
// Step 4: Register subtitle tracks
playlist.tracks?.forEach {
track ->
if (track.kind.equals("captions", true) || track.kind.equals("subtitles", true)) {
val subUrl = if (track.file.startsWith("//")) {
"https:${track.file}"
} else if (!track.file.startsWith("http")) {
"https://subscdn.top${track.file}"
} else {
track.file
}
subtitleCallback.invoke(
SubtitleFile(track.label, subUrl)
)
}
}
// If native flow found nothing, try fallback
if (!found) {
Log.w("NetflixMirror", "Native flow returned no sources for id=$id, trying fallback")
return loadLinksFallback(loadData, subtitleCallback, callback)
}
return true
}
// Fallback: TMDB embed API
private val net27Url = "https://net27.cc"
private val net27Referer = "https://videodownloader.site/"
private val net27Headers = mapOf(
"Accept" to "application/json",
"Referer" to net27Referer,
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
)
private suspend fun loadLinksFallback(
loadData: LoadData,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val tmdbId = loadData.tmdbId ?: run {
Log.e("NetflixMirror", "Fallback aborted: no tmdbId for '${loadData.title}'")
return false
}
val isMovie = loadData.season == null
val embedUrl = if (isMovie) {
"$net27Url/api/embed-tmdb/$tmdbId"
} else {
"$net27Url/api/embed-tmdb/$tmdbId?type=tv&s=${loadData.season}&e=${loadData.episode ?: 1}"
}
val response = try {
app.get(embedUrl, headers = net27Headers).parsed<Net27Response>()
} catch (e: Exception) {
Log.e("NetflixMirror", "embed-tmdb request failed: ${e.message}")
return false
}
if (response.ok != true) {
Log.e("NetflixMirror", "embed-tmdb returned not-ok")
return false
}
var found = false
response.streams?.forEach {
stream ->
callback.invoke(
newExtractorLink(name, "$name ${stream.resolution}p", stream.url, type = ExtractorLinkType.VIDEO) {
this.referer = net27Referer
this.quality = stream.resolution
}
)
found = true
}
if (response.streams.isNullOrEmpty() && !response.mp4.isNullOrBlank()) {
callback.invoke(
newExtractorLink(name, name, response.mp4, type = ExtractorLinkType.VIDEO) {
this.referer = net27Referer
this.quality = response.resolution?.toIntOrNull() ?: Qualities.Unknown.value
}
)
found = true
}
response.captions?.forEach {
caption ->
subtitleCallback.invoke(SubtitleFile(caption.name, caption.url))
}
if (!found && response.noSource == true) {
val message = response.error ?: "This title is still being added. Check back later."
throw ErrorLoadingException(message)
}
return found
}
// Cookie management for native flow
private suspend fun ensureNativeCookies(contentId: String) {
// If we already have the essential cookies, skip
if (nativeCookies.containsKey("user_token") &&
nativeCookies.containsKey("t_hash_p") &&
nativeCookies.containsKey("cf_clearance")
) {
return
}
// Warm up cookies by visiting the home page
// This should trigger Cloudflare challenge and set initial cookies
try {
val homeResp = app.get(
"$nativeOrigin/home",
headers = mapOf(
"User-Agent" to headers["User-Agent"]!!,
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language" to "en-IN,en-US;q=0.9,en;q=0.8"
)
)
// Extract cookies from response
extractCookiesFromResponse(homeResp)
// Also try to get the SE cookie by visiting the content page
val postResp = app.get(
"$mainUrl/mobile/post.php?id=$contentId&t=$unixTime",
headers = headers,
referer = "$mainUrl/home",
cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
)
extractCookiesFromResponse(postResp)
} catch (e: Exception) {
Log.w("NetflixMirror", "Cookie warmup failed: ${e.message}")
}
}
private fun extractCookiesFromResponse(response: NiceResponse) {
response.headers.values("Set-Cookie").forEach { cookieStr ->
val keyValue = cookieStr.split(";").firstOrNull()?.trim() ?: return@forEach
val parts = keyValue.split("=", limit = 2)
if (parts.size == 2) {
nativeCookies[parts[0]] = parts[1]
}
}
}
@Suppress("ObjectLiteralToLambda")
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val isNativeHost = request.url.host.contains("net52") ||
request.url.host.contains("net77") ||
request.url.host.contains("net22") ||
request.url.host.contains("net27")
// Use the link's own referer (fallback flow needs videodownloader.site),
// defaulting to the native referer when unset.
val referer = extractorLink.referer?.takeIf { it.isNotBlank() } ?: nativeReferer
val builder = request.newBuilder()
.header("Referer", referer)
if (isNativeHost) {
builder.header("Origin", nativeOrigin)
// The native CDN requires the hotlink/cf cookies collected during warmup.
val cookies = mutableMapOf<String, String>()
if (cookie_value.isNotEmpty()) cookies["t_hash_t"] = cookie_value
cookies.putAll(nativeCookies)
if (cookies.isNotEmpty()) {
builder.header("Cookie", cookies.entries.joinToString("; ") { "${it.key}=${it.value}" })
}
}
return chain.proceed(builder.build())
}
}
}
// Data classes
data class Id(val id: String)
data class LoadData(
val title: String,
val id: String,
val tmdbId: String? = null,
val season: Int? = null,
val episode: Int? = null
)
// Native flow responses
data class PlayResponse(val h: String? = null)
data class PlaylistResponse(
val title: String? = null,
val image2: String? = null,
val sources: List? = null,
val tracks: List? = null
)
data class PlaylistSource(
val file: String,
val label: String,
val type: String,
val default: String? = null
)
data class PlaylistTrack(
val kind: String,
val file: String,
val label: String,
val language: String? = null
)
// Fallback (TMDB embed) responses
data class Net27Response(
val ok: Boolean? = null,
val mp4: String? = null,
val resolution: String? = null,
val streams: List? = null,
val captions: List? = null,
val noSource: Boolean? = null,
val error: String? = null
)
data class Net27Stream(
val url: String,
val resolution: Int
)
data class Net27Caption(
val lang: String,
val name: String,
val url: String
)
}
Other details
No response
Acknowledgements
Describe your suggested feature
package com.horis.cncverse
import android.content.Context
import com.horis.cncverse.entities.EpisodesData
import com.horis.cncverse.entities.PostData
import com.horis.cncverse.entities.SearchData
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import okhttp3.Interceptor
import okhttp3.Response
import org.jsoup.nodes.Element
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.api.Log
import java.net.URLEncoder
import com.lagradost.nicehttp.NiceResponse
class NetflixMirrorProvider : MainAPI() {
companion object {
var context: Context? = null
}
override val supportedTypes = setOf(TvType.Movie, TvType.TvSeries)
override var lang = "hi"
override var mainUrl = "https://net52.cc"
private val newUrl = "https://net52.cc"
override var name = "Netflix"
override val hasMainPage = true
private var cookie_value = ""
private val headers = mapOf(
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language" to "en-IN,en-US;q=0.9,en;q=0.8",
"Cache-Control" to "max-age=0",
"Connection" to "keep-alive",
"sec-ch-ua" to ""Not(A:Brand";v="8", "Chromium";v="144", "Android WebView";v="144"",
"sec-ch-ua-mobile" to "?0",
"sec-ch-ua-platform" to ""Android"",
"Sec-Fetch-Dest" to "document",
"Sec-Fetch-Mode" to "navigate",
"Sec-Fetch-Site" to "same-origin",
"Sec-Fetch-User" to "?1",
"Upgrade-Insecure-Requests" to "1",
"User-Agent" to "Mozilla/5.0 (Linux; Android 13; Pixel 5 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/144.0.7559.132 Safari/537.36 /OS.Gatu v3.0",
"X-Requested-With" to "XMLHttpRequest"
)
// Native playlist flow URLs
private val playUrl = "https://net77.cc/play.php"
private val playlistBaseUrl = "https://net52.cc/playlist.php"
private val nativeReferer = "https://net77.cc/home"
private val nativeOrigin = "https://net77.cc"
// Cookie store for native flow
private var nativeCookies = mutableMapOf<String, String>()
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse? {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val cookies = mapOf("t_hash_t" to cookie_value, "ott" to "nf", "hd" to "on")
val document = app.get(
"$mainUrl/mobile/home?app=1",
cookies = cookies,
headers = headers,
referer = "$mainUrl/mobile/home?app=1"
).document
val items = document.select(".tray-container, #top10").map {
it.toHomePageList()
}
return newHomePageResponse(items, false)
}
private fun Element.toHomePageList(): HomePageList {
val name = select("h2, span").text()
val items = select("article, .top10-post").mapNotNull {
it.toSearchResult()
}
return HomePageList(name, items, isHorizontalImages = false)
}
private fun Element.toSearchResult(): SearchResponse? {
val id = selectFirst("a")?.attr("data-post") ?: attr("data-post")
if (id.isNullOrBlank()) return null
return newAnimeSearchResponse("", Id(id).toJson()) {
posterUrl = "https://imgcdn.kim/poster/v/$id.jpg"
posterHeaders = mapOf("Referer" to "$mainUrl/home")
}
}
override suspend fun search(query: String): List {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
val data = app.get(
"$mainUrl/mobile/search.php?s=$query&t=$unixTime",
referer = "$mainUrl/home",
cookies = cookies
).parsed()
return data.searchResult.map {
newAnimeSearchResponse(it.t, Id(it.id).toJson()) {
posterUrl = "https://imgcdn.kim/poster/v/${it.id}.jpg"
posterHeaders = mapOf("Referer" to "$mainUrl/home")
}
}
}
override suspend fun load(url: String): LoadResponse? {
cookie_value = if (cookie_value.isEmpty()) bypass(newUrl) else cookie_value
val id = parseJson(url).id
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
val data = app.get(
"$mainUrl/mobile/post.php?id=$id&t=$unixTime",
headers,
referer = "$mainUrl/home",
cookies = cookies
).parsed()
}
private suspend fun getEpisodes(
title: String, eid: String, sid: String, page: Int, tmdbId: String?
): List {
val episodes = arrayListOf()
val cookies = mapOf("t_hash_t" to cookie_value, "hd" to "on", "ott" to "nf")
var pg = page
while (true) {
val data = app.get(
"$mainUrl/mobile/episodes.php?s=$sid&series=$eid&t=$unixTime&page=$pg",
headers,
referer = "$mainUrl/home",
cookies = cookies
).parsed()
data.episodes?.mapTo(episodes) {
newEpisode(LoadData(
title, it.id, tmdbId,
it.s.replace("S", "").toIntOrNull(),
it.ep.replace("E", "").toIntOrNull()
)) {
name = it.t
episode = it.ep.replace("E", "").toIntOrNull()
season = it.s.replace("S", "").toIntOrNull()
this.posterUrl = "https://imgcdn.kim/poster/v/150/${it.id}.jpg"
this.runTime = it.time.replace("m", "").toIntOrNull()
}
}
if (data.nextPageShow == 0) break
pg++
}
return episodes
}
// Native play.php -> playlist.php flow with subtitles
override suspend fun loadLinks(
data: String,
isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val loadData = parseJson(data)
val id = loadData.id
}
// Fallback: TMDB embed API
private val net27Url = "https://net27.cc"
private val net27Referer = "https://videodownloader.site/"
private val net27Headers = mapOf(
"Accept" to "application/json",
"Referer" to net27Referer,
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
)
private suspend fun loadLinksFallback(
loadData: LoadData,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val tmdbId = loadData.tmdbId ?: run {
Log.e("NetflixMirror", "Fallback aborted: no tmdbId for '${loadData.title}'")
return false
}
val isMovie = loadData.season == null
}
// Cookie management for native flow
private suspend fun ensureNativeCookies(contentId: String) {
// If we already have the essential cookies, skip
if (nativeCookies.containsKey("user_token") &&
nativeCookies.containsKey("t_hash_p") &&
nativeCookies.containsKey("cf_clearance")
) {
return
}
}
private fun extractCookiesFromResponse(response: NiceResponse) {
response.headers.values("Set-Cookie").forEach { cookieStr ->
val keyValue = cookieStr.split(";").firstOrNull()?.trim() ?: return@forEach
val parts = keyValue.split("=", limit = 2)
if (parts.size == 2) {
nativeCookies[parts[0]] = parts[1]
}
}
}
@Suppress("ObjectLiteralToLambda")
override fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val isNativeHost = request.url.host.contains("net52") ||
request.url.host.contains("net77") ||
request.url.host.contains("net22") ||
request.url.host.contains("net27")
// Use the link's own referer (fallback flow needs videodownloader.site),
// defaulting to the native referer when unset.
val referer = extractorLink.referer?.takeIf { it.isNotBlank() } ?: nativeReferer
val builder = request.newBuilder()
.header("Referer", referer)
if (isNativeHost) {
builder.header("Origin", nativeOrigin)
// The native CDN requires the hotlink/cf cookies collected during warmup.
val cookies = mutableMapOf<String, String>()
if (cookie_value.isNotEmpty()) cookies["t_hash_t"] = cookie_value
cookies.putAll(nativeCookies)
if (cookies.isNotEmpty()) {
builder.header("Cookie", cookies.entries.joinToString("; ") { "${it.key}=${it.value}" })
}
}
return chain.proceed(builder.build())
}
}
}
// Data classes
data class Id(val id: String)
data class LoadData(
val title: String,
val id: String,
val tmdbId: String? = null,
val season: Int? = null,
val episode: Int? = null
)
// Native flow responses
data class PlayResponse(val h: String? = null)
data class PlaylistResponse(
val title: String? = null,
val image2: String? = null,
val sources: List? = null,
val tracks: List? = null
)
data class PlaylistSource(
val file: String,
val label: String,
val type: String,
val default: String? = null
)
data class PlaylistTrack(
val kind: String,
val file: String,
val label: String,
val language: String? = null
)
// Fallback (TMDB embed) responses
data class Net27Response(
val ok: Boolean? = null,
val mp4: String? = null,
val resolution: String? = null,
val streams: List? = null,
val captions: List? = null,
val noSource: Boolean? = null,
val error: String? = null
)
data class Net27Stream(
val url: String,
val resolution: Int
)
data class Net27Caption(
val lang: String,
val name: String,
val url: String
)
}
Other details
No response
Acknowledgements