Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ lifecycleKtx = "2.9.4"
material = "1.14.0-alpha08"
media3 = "1.8.0"
navigationKtx = "2.9.6"
newpipeextractor = "v0.24.8"
newpipeextractor = "v0.25.2"
nextlibMedia3 = "1.8.0-0.9.0"
nicehttp = "0.4.16"
overlappingpanels = "0.1.5"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,119 +1,39 @@
// Made For cs-kraptor By @trup40, @kraptor123, @ByAyzen
package com.lagradost.cloudstream3.extractors

import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.newAudioFile
import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.HlsPlaylistParser
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.newExtractorLink
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.net.URLDecoder
import org.schabi.newpipe.extractor.services.youtube.YoutubeService
import org.schabi.newpipe.extractor.stream.StreamInfo


class YoutubeShortLinkExtractor : YoutubeExtractor() {
class YoutubeShortLinkExtractor(
maxResolution: Int? = null
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxResolution is unnecessary and not needed. Please remove all references to maxResolution, as it is up to the caller to filter the output, not the callee.

Copy link
Author

@KaifTaufiq KaifTaufiq Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that, but i implement that just to avoid lagging on devices which don't support 4k
And since trailer player is using the same Extractor, the app can crash??
So i thought of adding a cap to it

) : YoutubeExtractor(maxResolution) {
override val mainUrl = "https://youtu.be"
}

class YoutubeMobileExtractor : YoutubeExtractor() {
class YoutubeMobileExtractor(
maxResolution: Int? = null
) : YoutubeExtractor(maxResolution) {
override val mainUrl = "https://m.youtube.com"
}

class YoutubeNoCookieExtractor : YoutubeExtractor() {
class YoutubeNoCookieExtractor(
maxResolution: Int? = null
) : YoutubeExtractor(maxResolution) {
override val mainUrl = "https://www.youtube-nocookie.com"
}

open class YoutubeExtractor : ExtractorApi() {
open class YoutubeExtractor(
private val maxResolution: Int? = null
) : ExtractorApi() {

override val mainUrl = "https://www.youtube.com"
override val requiresReferer = false
override val name = "YouTube"
private val youtubeUrl = "https://www.youtube.com"

companion object {
private const val USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15"
private val HEADERS = mapOf(
"User-Agent" to USER_AGENT,
"Accept-Language" to "en-US,en;q=0.5"
)
}


private fun extractYtCfg(html: String): String? {
val regex = Regex("""ytcfg\.set\(\s*(\{.*?\})\s*\)\s*;""")
val match = regex.find(html)
return match?.groupValues?.getOrNull(1)
}

data class PageConfig(
@JsonProperty("INNERTUBE_API_KEY")
val apiKey: String,
@JsonProperty("INNERTUBE_CLIENT_VERSION")
val clientVersion: String = "2.20240725.01.00",
@JsonProperty("VISITOR_DATA")
val visitorData: String = ""
)

private suspend fun getPageConfig(videoId: String): PageConfig? =
tryParseJson(extractYtCfg(app.get("$mainUrl/watch?v=$videoId", headers = HEADERS).text))

fun extractYouTubeId(url: String): String {
return when {
url.contains("oembed") && url.contains("url=") -> {
val encodedUrl = url.substringAfter("url=").substringBefore("&")
val decodedUrl = URLDecoder.decode(encodedUrl, "UTF-8")
extractYouTubeId(decodedUrl)
}

url.contains("attribution_link") && url.contains("u=") -> {
val encodedUrl = url.substringAfter("u=").substringBefore("&")
val decodedUrl = URLDecoder.decode(encodedUrl, "UTF-8")
extractYouTubeId(decodedUrl)
}

url.contains("watch?v=") -> url.substringAfter("watch?v=").substringBefore("&")
.substringBefore("#")

url.contains("&v=") -> url.substringAfter("&v=").substringBefore("&")
.substringBefore("#")

url.contains("youtu.be/") -> url.substringAfter("youtu.be/").substringBefore("?")
.substringBefore("#").substringBefore("&")

url.contains("/embed/") -> url.substringAfter("/embed/").substringBefore("?")
.substringBefore("#")

url.contains("/v/") -> url.substringAfter("/v/").substringBefore("?")
.substringBefore("#")

url.contains("/e/") -> url.substringAfter("/e/").substringBefore("?")
.substringBefore("#")

url.contains("/shorts/") -> url.substringAfter("/shorts/").substringBefore("?")
.substringBefore("#")

url.contains("/live/") -> url.substringAfter("/live/").substringBefore("?")
.substringBefore("#")

url.contains("/watch/") -> url.substringAfter("/watch/").substringBefore("?")
.substringBefore("#")

url.contains("watch%3Fv%3D") -> url.substringAfter("watch%3Fv%3D")
.substringBefore("%26").substringBefore("#")

url.contains("v%3D") -> url.substringAfter("v%3D").substringBefore("%26")
.substringBefore("#")

else -> error("No Id Found")
}
}

override val requiresReferer = false

override suspend fun getUrl(
url: String,
Expand All @@ -122,162 +42,81 @@ open class YoutubeExtractor : ExtractorApi() {
callback: (ExtractorLink) -> Unit
) {
val videoId = extractYouTubeId(url)
val config = getPageConfig(videoId) ?: return
val watchUrl = "$mainUrl/watch?v=$videoId"

val jsonBody = """
{
"context": {
"client": {
"hl": "en",
"gl": "US",
"clientName": "WEB",
"clientVersion": "${config.clientVersion}",
"visitorData": "${config.visitorData}",
"platform": "DESKTOP",
"userAgent": "$USER_AGENT"
}
},
"videoId": "$videoId",
"playbackContext": {
"contentPlaybackContext": {
"html5Preference": "HTML5_PREF_WANTS"
}
}
}
""".toRequestBody("application/json; charset=utf-8".toMediaType())

val response =
app.post(
"$youtubeUrl/youtubei/v1/player?key=${config.apiKey}",
headers = HEADERS,
requestBody = jsonBody
).parsed<Root>()

val captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks
val streamInfo = StreamInfo.getInfo(YoutubeService(0), watchUrl)

if (captionTracks != null) {
for (caption in captionTracks) {
subtitleCallback.invoke(
newSubtitleFile(
lang =caption.name.simpleText,
url ="${caption.baseUrl}&fmt=ttml" // The default format is not supported
) { headers = HEADERS })
}
}

val hlsUrl = response.streamingData.hlsManifestUrl
val getHls = app.get(hlsUrl, headers = HEADERS).text
val playlist = HlsPlaylistParser.parse(hlsUrl, getHls) ?: return
processStreams(streamInfo, subtitleCallback, callback)
}

var variantIndex = 0
for (tag in playlist.tags) {
val trimmedTag = tag.trim()
if (!trimmedTag.startsWith("#EXT-X-STREAM-INF")) {
continue
}
val variant = playlist.variants.getOrNull(variantIndex++) ?: continue
private suspend fun processStreams(
info: StreamInfo,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {

val audioId = trimmedTag.split(",")
.find { it.trim().startsWith("YT-EXT-AUDIO-CONTENT-ID=") }
?.split("=")
?.get(1)
?.trim('"') ?: ""
val videoStreams = info.videoOnlyStreams
?.filterByResolution(maxResolution)
?: emptyList()

val langString =
SubtitleHelper.fromTagToEnglishLanguageName(
audioId.substringBefore(".")
) ?: SubtitleHelper.fromTagToEnglishLanguageName(
audioId.substringBefore("-")
) ?: audioId
if (videoStreams.isEmpty()) return false

val url = variant.url.toString()
val audioStreams = info.audioStreams.orEmpty()

if (url.isBlank()) {
continue
}
videoStreams.forEach { video ->

callback.invoke(
callback(
newExtractorLink(
source = this.name,
name = "Youtube${if (langString.isNotBlank()) " $langString" else ""}",
url = url,
type = ExtractorLinkType.M3U8
source = name,
name = "YouTube ${normalizeCodec(video.codec)}",
url = video.content
) {
this.referer = "${mainUrl}/"
this.quality = variant.format.height
quality = video.height
audioTracks = audioStreams.map { newAudioFile(it.content) }
}
)
}
}


private data class Root(
// val responseContext: ResponseContext,
// val playabilityStatus: PlayabilityStatus,
@JsonProperty("streamingData")
val streamingData: StreamingData,
// val playbackTracking: PlaybackTracking,
@JsonProperty("captions")
val captions: Captions?,
// val videoDetails: VideoDetails,
// val annotations: List<Annotation>,
// val playerConfig: PlayerConfig,
// val storyboards: Storyboards,
// val microformat: Microformat,
// val cards: Cards,
// val trackingParams: String,
// val endscreen: Endscreen,
// val paidContentOverlay: PaidContentOverlay,
// val adPlacements: List<AdPlacement>,
// val adBreakHeartbeatParams: String,
// val frameworkUpdates: FrameworkUpdates,
)
info.subtitles.forEach { subtitle ->
subtitleCallback(
newSubtitleFile(
lang = subtitle.displayLanguageName
?: subtitle.languageTag
?: "Unknown",
url = subtitle.content
)
)
}

return true
}

private data class StreamingData(
//val expiresInSeconds: String,
//val formats: List<Format>,
//val adaptiveFormats: List<AdaptiveFormat>,
@JsonProperty("hlsManifestUrl")
val hlsManifestUrl: String,
//val serverAbrStreamingUrl: String,
)
// ---------------- HELPERS ----------------

private data class Captions(
@JsonProperty("playerCaptionsTracklistRenderer")
val playerCaptionsTracklistRenderer: PlayerCaptionsTracklistRenderer?,
)
private fun extractYouTubeId(url: String): String {
val regex = Regex(
"(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})"
)
return regex.find(url)?.groupValues?.get(1)
?: throw IllegalArgumentException("Invalid YouTube URL: $url")
}

private data class PlayerCaptionsTracklistRenderer(
@JsonProperty("captionTracks")
val captionTracks: List<CaptionTrack>?,
//val audioTracks: List<AudioTrack>,
//val translationLanguages: List<TranslationLanguage>,
//@JsonProperty("defaultAudioTrackIndex")
//val defaultAudioTrackIndex: Long,
)
private fun List<org.schabi.newpipe.extractor.stream.VideoStream>.filterByResolution(
max: Int?
) = if (max == null) this else filter { it.height <= max }

private data class CaptionTrack(
@JsonProperty("baseUrl")
val baseUrl: String,
@JsonProperty("name")
val name: Name,
//val vssId: String,
//val languageCode: String,
//val kind: String?,
//val isTranslatable: Boolean,
//val trackName: String,
)
private fun normalizeCodec(codec: String?): String {
if (codec.isNullOrBlank()) return ""

private data class Name(
@JsonProperty("simpleText")
val simpleText: String,
)
val c = codec.lowercase()

// data class AudioTrack(
// val captionTrackIndices: List<Long>,
// val defaultCaptionTrackIndex: Long,
// val hasDefaultTrack: Boolean,
// val audioTrackId: String,
// val captionsInitialState: String,
// )
return when {
c.startsWith("av01") -> "AV1"
c.startsWith("vp9") -> "VP9"
c.startsWith("avc1") || c.startsWith("h264") -> "H264"
c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265"
else -> codec.substringBefore('.').uppercase()
}
}
}