owneraj
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 25/100
- Loại issue
- Tính năng
- Độ rõ ràng
- Cần làm rõ
- Mức độ hoạt động
- Sôi nổi
- Lĩnh vực
- mobile-dev
Hướng nghiên cứu
Start by reviewing the proposed NetflixMirrorProvider class, especially getMainPage, search, load, and loadLinks, and compare it with existing provider implementations in the repository. Run the project’s Android build and provider tests to identify integration requirements; done means the provider is accepted, builds successfully, and its search, metadata, episode, playback, and subtitle flows work.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
- My suggestion is NOT about adding a new provider
- I have searched the existing issues and this is a new ticket, NOT a duplicate or related to another open issue.
- Ngôn ngữ chính
- Kotlin
- Star
- 10.7k
- Fork
- 1.1k
- Merge trung bình
- 3 ngày 3 giờ
- Pull request đã merge (30 ngày)
- 23
Chuẩn bị môi trường
Chúng tôi chưa kiểm tra các tệp thiết lập môi trường của dự án này. Hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của recloudstream/cloudstream
-
Add new fonts for subtitlesĐang mởenhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
recloudstream/cloudstream#3100 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
recloudstream/cloudstream#2639 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement potential extension
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
recloudstream/cloudstream#3232 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
enhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
recloudstream/cloudstream#3231 · 2 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Home, UI, Download, ExtentionĐang mởenhancement
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
recloudstream/cloudstream#3230 · 3 bình luận · 5 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của recloudstream/cloudstream
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
mobile-dev-inc/Maestro#3646 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
-
enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
GetStream/stream-chat-android#6762 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
SimonHalvdansson/Harmonic-HN#363 ·