From 137a7ffc0cec361c409fed1f197269ce81be71e0 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Mon, 9 Feb 2026 23:45:33 +0530 Subject: [PATCH 1/3] newpipeextractor bump --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d32aad375e..afceb26dde 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" From f794f3a884244ec827ee0dfcf145679603dcadc3 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Tue, 10 Feb 2026 03:56:16 +0530 Subject: [PATCH 2/3] Youtube Extractor Updated with NewPipe --- .../extractors/YoutubeExtractor.kt | 307 +++++------------- 1 file changed, 73 insertions(+), 234 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt index 0ace27a317..cb464fc5b3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -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 +) : 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, @@ -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() - - 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, - // val playerConfig: PlayerConfig, - // val storyboards: Storyboards, - // val microformat: Microformat, - // val cards: Cards, - // val trackingParams: String, - // val endscreen: Endscreen, - // val paidContentOverlay: PaidContentOverlay, - // val adPlacements: List, - // 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, - //val adaptiveFormats: List, - @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?, - //val audioTracks: List, - //val translationLanguages: List, - //@JsonProperty("defaultAudioTrackIndex") - //val defaultAudioTrackIndex: Long, - ) + private fun List.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, -// 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() + } + } } \ No newline at end of file From e4556894ad246e5d0e2491273819f40dee5133d8 Mon Sep 17 00:00:00 2001 From: Kaif Shaikh Date: Wed, 11 Feb 2026 07:28:44 +0530 Subject: [PATCH 3/3] Remove unwanted YoutubeService --- .../com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt index cb464fc5b3..44de188473 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -6,7 +6,6 @@ import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink -import org.schabi.newpipe.extractor.services.youtube.YoutubeService import org.schabi.newpipe.extractor.stream.StreamInfo class YoutubeShortLinkExtractor( @@ -44,7 +43,7 @@ open class YoutubeExtractor( val videoId = extractYouTubeId(url) val watchUrl = "$mainUrl/watch?v=$videoId" - val streamInfo = StreamInfo.getInfo(YoutubeService(0), watchUrl) + val streamInfo = StreamInfo.getInfo(watchUrl) processStreams(streamInfo, subtitleCallback, callback) }