1- // Made For cs-kraptor By @trup40, @kraptor123, @ByAyzen
21package com.lagradost.cloudstream3.extractors
32
4- import com.fasterxml.jackson.annotation.JsonProperty
53import com.lagradost.cloudstream3.SubtitleFile
6- import com.lagradost.cloudstream3.app
4+ import com.lagradost.cloudstream3.newAudioFile
75import com.lagradost.cloudstream3.newSubtitleFile
8- import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
96import com.lagradost.cloudstream3.utils.ExtractorApi
107import com.lagradost.cloudstream3.utils.ExtractorLink
11- import com.lagradost.cloudstream3.utils.ExtractorLinkType
12- import com.lagradost.cloudstream3.utils.HlsPlaylistParser
13- import com.lagradost.cloudstream3.utils.SubtitleHelper
148import com.lagradost.cloudstream3.utils.newExtractorLink
15- import okhttp3.MediaType.Companion.toMediaType
16- import okhttp3.RequestBody.Companion.toRequestBody
17- import java.net.URLDecoder
9+ import org.schabi.newpipe.extractor.stream.StreamInfo
1810
19-
20- class YoutubeShortLinkExtractor : YoutubeExtractor () {
11+ class YoutubeShortLinkExtractor (
12+ maxResolution : Int? = null
13+ ) : YoutubeExtractor(maxResolution) {
2114 override val mainUrl = " https://youtu.be"
2215}
2316
24- class YoutubeMobileExtractor : YoutubeExtractor () {
17+ class YoutubeMobileExtractor (
18+ maxResolution : Int? = null
19+ ) : YoutubeExtractor(maxResolution) {
2520 override val mainUrl = " https://m.youtube.com"
2621}
2722
28- class YoutubeNoCookieExtractor : YoutubeExtractor () {
23+ class YoutubeNoCookieExtractor (
24+ maxResolution : Int? = null
25+ ) : YoutubeExtractor(maxResolution) {
2926 override val mainUrl = " https://www.youtube-nocookie.com"
3027}
3128
32- open class YoutubeExtractor : ExtractorApi () {
29+ open class YoutubeExtractor (
30+ private val maxResolution : Int? = null
31+ ) : ExtractorApi() {
32+
3333 override val mainUrl = " https://www.youtube.com"
34- override val requiresReferer = false
3534 override val name = " YouTube"
36- private val youtubeUrl = " https://www.youtube.com"
37-
38- companion object {
39- private const val USER_AGENT =
40- " 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"
41- private val HEADERS = mapOf (
42- " User-Agent" to USER_AGENT ,
43- " Accept-Language" to " en-US,en;q=0.5"
44- )
45- }
46-
47-
48- private fun extractYtCfg (html : String ): String? {
49- val regex = Regex (""" ytcfg\.set\(\s*(\{.*?\})\s*\)\s*;""" )
50- val match = regex.find(html)
51- return match?.groupValues?.getOrNull(1 )
52- }
53-
54- data class PageConfig (
55- @JsonProperty(" INNERTUBE_API_KEY" )
56- val apiKey : String ,
57- @JsonProperty(" INNERTUBE_CLIENT_VERSION" )
58- val clientVersion : String = " 2.20240725.01.00" ,
59- @JsonProperty(" VISITOR_DATA" )
60- val visitorData : String = " "
61- )
62-
63- private suspend fun getPageConfig (videoId : String ): PageConfig ? =
64- tryParseJson(extractYtCfg(app.get(" $mainUrl /watch?v=$videoId " , headers = HEADERS ).text))
65-
66- fun extractYouTubeId (url : String ): String {
67- return when {
68- url.contains(" oembed" ) && url.contains(" url=" ) -> {
69- val encodedUrl = url.substringAfter(" url=" ).substringBefore(" &" )
70- val decodedUrl = URLDecoder .decode(encodedUrl, " UTF-8" )
71- extractYouTubeId(decodedUrl)
72- }
73-
74- url.contains(" attribution_link" ) && url.contains(" u=" ) -> {
75- val encodedUrl = url.substringAfter(" u=" ).substringBefore(" &" )
76- val decodedUrl = URLDecoder .decode(encodedUrl, " UTF-8" )
77- extractYouTubeId(decodedUrl)
78- }
79-
80- url.contains(" watch?v=" ) -> url.substringAfter(" watch?v=" ).substringBefore(" &" )
81- .substringBefore(" #" )
82-
83- url.contains(" &v=" ) -> url.substringAfter(" &v=" ).substringBefore(" &" )
84- .substringBefore(" #" )
85-
86- url.contains(" youtu.be/" ) -> url.substringAfter(" youtu.be/" ).substringBefore(" ?" )
87- .substringBefore(" #" ).substringBefore(" &" )
88-
89- url.contains(" /embed/" ) -> url.substringAfter(" /embed/" ).substringBefore(" ?" )
90- .substringBefore(" #" )
91-
92- url.contains(" /v/" ) -> url.substringAfter(" /v/" ).substringBefore(" ?" )
93- .substringBefore(" #" )
94-
95- url.contains(" /e/" ) -> url.substringAfter(" /e/" ).substringBefore(" ?" )
96- .substringBefore(" #" )
97-
98- url.contains(" /shorts/" ) -> url.substringAfter(" /shorts/" ).substringBefore(" ?" )
99- .substringBefore(" #" )
100-
101- url.contains(" /live/" ) -> url.substringAfter(" /live/" ).substringBefore(" ?" )
102- .substringBefore(" #" )
103-
104- url.contains(" /watch/" ) -> url.substringAfter(" /watch/" ).substringBefore(" ?" )
105- .substringBefore(" #" )
106-
107- url.contains(" watch%3Fv%3D" ) -> url.substringAfter(" watch%3Fv%3D" )
108- .substringBefore(" %26" ).substringBefore(" #" )
109-
110- url.contains(" v%3D" ) -> url.substringAfter(" v%3D" ).substringBefore(" %26" )
111- .substringBefore(" #" )
112-
113- else -> error(" No Id Found" )
114- }
115- }
116-
35+ override val requiresReferer = false
11736
11837 override suspend fun getUrl (
11938 url : String ,
@@ -122,162 +41,81 @@ open class YoutubeExtractor : ExtractorApi() {
12241 callback : (ExtractorLink ) -> Unit
12342 ) {
12443 val videoId = extractYouTubeId(url)
125- val config = getPageConfig(videoId) ? : return
44+ val watchUrl = " $mainUrl /watch?v= $videoId "
12645
127- val jsonBody = """
128- {
129- "context": {
130- "client": {
131- "hl": "en",
132- "gl": "US",
133- "clientName": "WEB",
134- "clientVersion": "${config.clientVersion} ",
135- "visitorData": "${config.visitorData} ",
136- "platform": "DESKTOP",
137- "userAgent": "$USER_AGENT "
138- }
139- },
140- "videoId": "$videoId ",
141- "playbackContext": {
142- "contentPlaybackContext": {
143- "html5Preference": "HTML5_PREF_WANTS"
144- }
145- }
146- }
147- """ .toRequestBody(" application/json; charset=utf-8" .toMediaType())
148-
149- val response =
150- app.post(
151- " $youtubeUrl /youtubei/v1/player?key=${config.apiKey} " ,
152- headers = HEADERS ,
153- requestBody = jsonBody
154- ).parsed<Root >()
155-
156- val captionTracks = response.captions?.playerCaptionsTracklistRenderer?.captionTracks
46+ val streamInfo = StreamInfo .getInfo(watchUrl)
15747
158- if (captionTracks != null ) {
159- for (caption in captionTracks) {
160- subtitleCallback.invoke(
161- newSubtitleFile(
162- lang = caption.name.simpleText,
163- url = " ${caption.baseUrl} &fmt=ttml" // The default format is not supported
164- ) { headers = HEADERS })
165- }
166- }
167-
168- val hlsUrl = response.streamingData.hlsManifestUrl
169- val getHls = app.get(hlsUrl, headers = HEADERS ).text
170- val playlist = HlsPlaylistParser .parse(hlsUrl, getHls) ? : return
48+ processStreams(streamInfo, subtitleCallback, callback)
49+ }
17150
172- var variantIndex = 0
173- for (tag in playlist.tags) {
174- val trimmedTag = tag.trim()
175- if (! trimmedTag.startsWith(" #EXT-X-STREAM-INF" )) {
176- continue
177- }
178- val variant = playlist.variants.getOrNull(variantIndex++ ) ? : continue
51+ private suspend fun processStreams (
52+ info : StreamInfo ,
53+ subtitleCallback : (SubtitleFile ) -> Unit ,
54+ callback : (ExtractorLink ) -> Unit
55+ ): Boolean {
17956
180- val audioId = trimmedTag.split(" ," )
181- .find { it.trim().startsWith(" YT-EXT-AUDIO-CONTENT-ID=" ) }
182- ?.split(" =" )
183- ?.get(1 )
184- ?.trim(' "' ) ? : " "
57+ val videoStreams = info.videoOnlyStreams
58+ ?.filterByResolution(maxResolution)
59+ ? : emptyList()
18560
186- val langString =
187- SubtitleHelper .fromTagToEnglishLanguageName(
188- audioId.substringBefore(" ." )
189- ) ? : SubtitleHelper .fromTagToEnglishLanguageName(
190- audioId.substringBefore(" -" )
191- ) ? : audioId
61+ if (videoStreams.isEmpty()) return false
19262
193- val url = variant.url.toString ()
63+ val audioStreams = info.audioStreams.orEmpty ()
19464
195- if (url.isBlank()) {
196- continue
197- }
65+ videoStreams.forEach { video ->
19866
199- callback.invoke (
67+ callback(
20068 newExtractorLink(
201- source = this .name,
202- name = " Youtube${if (langString.isNotBlank()) " $langString " else " " } " ,
203- url = url,
204- type = ExtractorLinkType .M3U8
69+ source = name,
70+ name = " YouTube ${normalizeCodec(video.codec)} " ,
71+ url = video.content
20572 ) {
206- this .referer = " ${mainUrl} / "
207- this .quality = variant.format.height
73+ quality = video.height
74+ audioTracks = audioStreams.map { newAudioFile(it.content) }
20875 }
20976 )
21077 }
211- }
21278
21379
214- private data class Root (
215- // val responseContext: ResponseContext,
216- // val playabilityStatus: PlayabilityStatus,
217- @JsonProperty(" streamingData" )
218- val streamingData : StreamingData ,
219- // val playbackTracking: PlaybackTracking,
220- @JsonProperty(" captions" )
221- val captions : Captions ? ,
222- // val videoDetails: VideoDetails,
223- // val annotations: List<Annotation>,
224- // val playerConfig: PlayerConfig,
225- // val storyboards: Storyboards,
226- // val microformat: Microformat,
227- // val cards: Cards,
228- // val trackingParams: String,
229- // val endscreen: Endscreen,
230- // val paidContentOverlay: PaidContentOverlay,
231- // val adPlacements: List<AdPlacement>,
232- // val adBreakHeartbeatParams: String,
233- // val frameworkUpdates: FrameworkUpdates,
234- )
80+ info.subtitles.forEach { subtitle ->
81+ subtitleCallback(
82+ newSubtitleFile(
83+ lang = subtitle.displayLanguageName
84+ ? : subtitle.languageTag
85+ ? : " Unknown" ,
86+ url = subtitle.content
87+ )
88+ )
89+ }
90+
91+ return true
92+ }
23593
236- private data class StreamingData (
237- // val expiresInSeconds: String,
238- // val formats: List<Format>,
239- // val adaptiveFormats: List<AdaptiveFormat>,
240- @JsonProperty(" hlsManifestUrl" )
241- val hlsManifestUrl : String ,
242- // val serverAbrStreamingUrl: String,
243- )
94+ // ---------------- HELPERS ----------------
24495
245- private data class Captions (
246- @JsonProperty(" playerCaptionsTracklistRenderer" )
247- val playerCaptionsTracklistRenderer : PlayerCaptionsTracklistRenderer ? ,
248- )
96+ private fun extractYouTubeId (url : String ): String {
97+ val regex = Regex (
98+ " (?:youtu\\ .be/|youtube(?:-nocookie)?\\ .com/(?:.*v=|v/|u/\\ w/|embed/|shorts/|live/))([\\ w-]{11})"
99+ )
100+ return regex.find(url)?.groupValues?.get(1 )
101+ ? : throw IllegalArgumentException (" Invalid YouTube URL: $url " )
102+ }
249103
250- private data class PlayerCaptionsTracklistRenderer (
251- @JsonProperty(" captionTracks" )
252- val captionTracks : List <CaptionTrack >? ,
253- // val audioTracks: List<AudioTrack>,
254- // val translationLanguages: List<TranslationLanguage>,
255- // @JsonProperty("defaultAudioTrackIndex")
256- // val defaultAudioTrackIndex: Long,
257- )
104+ private fun List<org.schabi.newpipe.extractor.stream.VideoStream>.filterByResolution (
105+ max : Int?
106+ ) = if (max == null ) this else filter { it.height <= max }
258107
259- private data class CaptionTrack (
260- @JsonProperty(" baseUrl" )
261- val baseUrl : String ,
262- @JsonProperty(" name" )
263- val name : Name ,
264- // val vssId: String,
265- // val languageCode: String,
266- // val kind: String?,
267- // val isTranslatable: Boolean,
268- // val trackName: String,
269- )
108+ private fun normalizeCodec (codec : String? ): String {
109+ if (codec.isNullOrBlank()) return " "
270110
271- private data class Name (
272- @JsonProperty(" simpleText" )
273- val simpleText : String ,
274- )
111+ val c = codec.lowercase()
275112
276- // data class AudioTrack(
277- // val captionTrackIndices: List<Long>,
278- // val defaultCaptionTrackIndex: Long,
279- // val hasDefaultTrack: Boolean,
280- // val audioTrackId: String,
281- // val captionsInitialState: String,
282- // )
113+ return when {
114+ c.startsWith(" av01" ) -> " AV1"
115+ c.startsWith(" vp9" ) -> " VP9"
116+ c.startsWith(" avc1" ) || c.startsWith(" h264" ) -> " H264"
117+ c.startsWith(" hev1" ) || c.startsWith(" hvc1" ) || c.startsWith(" hevc" ) -> " H265"
118+ else -> codec.substringBefore(' .' ).uppercase()
119+ }
120+ }
283121}
0 commit comments