Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

owneraj

Đã đóng
#3,225 0 bình luận 1 reaction 0 người được giao Xem trên GitHub

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
Công nghệ
android, kotlin
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ả

enhancement
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

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. 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.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của recloudstream/cloudstream

Tất cả issue của recloudstream/cloudstream

Issue tương tự

Thêm issue về Kotlin

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.