Skip to content
Merged
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,21 +1,13 @@
// 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 com.lagradost.cloudstream3.utils.ExtractorLinkType
import org.schabi.newpipe.extractor.stream.StreamInfo

class YoutubeShortLinkExtractor : YoutubeExtractor() {
override val mainUrl = "https://youtu.be"
Expand All @@ -30,90 +22,10 @@ class YoutubeNoCookieExtractor : YoutubeExtractor() {
}

open class YoutubeExtractor : 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 +34,89 @@ open class YoutubeExtractor : ExtractorApi() {
callback: (ExtractorLink) -> Unit
) {
val videoId = extractYouTubeId(url)
val config = getPageConfig(videoId) ?: return

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 watchUrl = "$mainUrl/watch?v=$videoId"

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

val captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks
val isLive = info.streamType?.name.equals("LIVE_STREAM")

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 })
}
if( isLive && info.hlsUrl != null ) {
callback(
newExtractorLink(
source = name,
name = "YouTube Live",
url = info.hlsUrl
) {
type = ExtractorLinkType.M3U8
}
)
} else {
processVideo(info, subtitleCallback, callback)
}
}

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

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 processVideo(
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.orEmpty()

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
)
)
}

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

private data class Captions(
@JsonProperty("playerCaptionsTracklistRenderer")
val playerCaptionsTracklistRenderer: PlayerCaptionsTracklistRenderer?,
)
// ---------------- HELPERS ----------------

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 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 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()
}
}
}