11// Made For cs-kraptor By @trup40, @kraptor123, @ByAyzen
22package com.lagradost.cloudstream3.extractors
33
4+ import com.fasterxml.jackson.annotation.JsonProperty
45import com.lagradost.cloudstream3.SubtitleFile
56import com.lagradost.cloudstream3.app
6- import com.lagradost.cloudstream3.mvvm.logError
7+ import com.lagradost.cloudstream3.newSubtitleFile
8+ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
79import com.lagradost.cloudstream3.utils.ExtractorApi
810import com.lagradost.cloudstream3.utils.ExtractorLink
911import com.lagradost.cloudstream3.utils.ExtractorLinkType
1012import com.lagradost.cloudstream3.utils.HlsPlaylistParser
1113import com.lagradost.cloudstream3.utils.SubtitleHelper
1214import com.lagradost.cloudstream3.utils.newExtractorLink
13- import kotlinx.coroutines.Dispatchers
14- import kotlinx.coroutines.IO
15- import kotlinx.coroutines.withContext
1615import okhttp3.MediaType.Companion.toMediaType
1716import okhttp3.RequestBody.Companion.toRequestBody
18- import org.json.JSONObject
1917import java.net.URLDecoder
2018
2119
@@ -31,15 +29,14 @@ class YoutubeNoCookieExtractor : YoutubeExtractor() {
3129 override val mainUrl = " https://www.youtube-nocookie.com"
3230}
3331
34-
3532open class YoutubeExtractor : ExtractorApi () {
3633 override val mainUrl = " https://www.youtube.com"
3734 override val requiresReferer = false
3835 override val name = " YouTube"
3936 private val youtubeUrl = " https://www.youtube.com"
4037
4138 companion object {
42- private val USER_AGENT =
39+ private const val USER_AGENT =
4340 " 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"
4441 private val HEADERS = mapOf (
4542 " User-Agent" to USER_AGENT ,
@@ -48,43 +45,23 @@ open class YoutubeExtractor : ExtractorApi() {
4845 }
4946
5047
51- private fun extractYtCfg (html : String ): JSONObject ? {
52- try {
53- val regex = Regex (""" ytcfg\.set\(\s*(\{.*?\})\s*\)\s*;""" )
54- val match = regex.find(html)
55- if (match != null ) {
56- return JSONObject (match.groupValues[1 ])
57- }
58- } catch (e: Exception ) {
59- logError(e)
60- }
61- return null
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 )
6252 }
6353
64- private suspend fun getPageConfig (videoId : String? = null): Map <String , String >? =
65- withContext(Dispatchers .IO ) {
66- try {
67- val url = if (videoId != null ) " $mainUrl /watch?v=$videoId " else mainUrl
68- val response = app.get(url, headers = HEADERS )
69- val html = response.text
70- val ytCfg = extractYtCfg(html) ? : return @withContext null
71-
72- val apiKey = ytCfg.optString(" INNERTUBE_API_KEY" )
73- val clientVersion = ytCfg.optString(" INNERTUBE_CLIENT_VERSION" , " 2.20240725.01.00" )
74- val visitorData = ytCfg.optString(" VISITOR_DATA" , " " )
75-
76- if (apiKey.isNotEmpty()) {
77- return @withContext mapOf (
78- " apiKey" to apiKey,
79- " clientVersion" to clientVersion,
80- " visitorData" to visitorData
81- )
82- }
83- } catch (e: Exception ) {
84- logError(e)
85- }
86- return @withContext null
87- }
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))
8865
8966 fun extractYouTubeId (url : String ): String {
9067 return when {
@@ -145,24 +122,17 @@ open class YoutubeExtractor : ExtractorApi() {
145122 callback : (ExtractorLink ) -> Unit
146123 ) {
147124 val videoId = extractYouTubeId(url)
148-
149125 val config = getPageConfig(videoId) ? : return
150126
151- val apiKey = config[" apiKey" ]
152- val clientVersion = config[" clientVersion" ]
153- val visitorData = config[" visitorData" ]
154-
155- val apiUrl = " $youtubeUrl /youtubei/v1/player?key=$apiKey "
156-
157127 val jsonBody = """
158128 {
159129 "context": {
160130 "client": {
161131 "hl": "en",
162132 "gl": "US",
163133 "clientName": "WEB",
164- "clientVersion": "$clientVersion ",
165- "visitorData": "$visitorData ",
134+ "clientVersion": "${config. clientVersion} ",
135+ "visitorData": "${config. visitorData} ",
166136 "platform": "DESKTOP",
167137 "userAgent": "$USER_AGENT "
168138 }
@@ -174,115 +144,136 @@ open class YoutubeExtractor : ExtractorApi() {
174144 }
175145 }
176146 }
177- """
178-
179- try {
180- val mediaType = " application/json; charset=utf-8" .toMediaType()
181- val requestBody = jsonBody.toRequestBody(mediaType)
182-
183- val response = app.post(apiUrl, headers = HEADERS , requestBody = requestBody)
184- val jsonResponse = JSONObject (response.text)
185-
186- /*
187- Subtitles Not Working Help Wanted
188-
189- val subtitles = mapper.readValue<Captions>(response.text)
190-
191- subtitles.captions.playerCaptionsTracklistRenderer.captionTracks?.forEach { subtitle ->
192- val url = subtitle.baseUrl ?: ""
193- val lang = subtitle.name?.simpleText ?: ""
194- subtitleCallback.invoke(newSubtitleFile(
195- lang = lang, url = url
196- ) {
197- this.headers = HEADERS
198- })
199- }
200-
201- */
202-
203- val streamingData = jsonResponse.optJSONObject(" streamingData" )
204-
205- if (streamingData != null ) {
206- val hlsUrl = streamingData.optString(" hlsManifestUrl" )
207- val getHls = app.get(hlsUrl, HEADERS ).text
208-
209- val playlist = HlsPlaylistParser .parse(hlsUrl, getHls)
210-
211- playlist?.let { playL ->
212- var variantIndex = 0
213-
214- playL.tags.forEach { tag ->
215- val trimmedTag = tag.trim()
216-
217- if (trimmedTag.startsWith(" #EXT-X-STREAM-INF" )) {
218-
219- if (variantIndex < playL.variants.size) {
220- val variant = playL.variants[variantIndex]
221-
222- val audioId = trimmedTag.split(" ," )
223- .find { it.trim().startsWith(" YT-EXT-AUDIO-CONTENT-ID=" ) }
224- ?.split(" =" )
225- ?.get(1 )
226- ?.trim(' "' )
227-
228- val langString = if (! audioId.isNullOrEmpty()) {
229- val lang = audioId.substringBefore(" ." )
230- SubtitleHelper .fromTagToEnglishLanguageName(lang)
231- } else {
232- " "
233- }
234-
235- val height = variant.format.height
236-
237- val url = variant.url.toString()
238-
239- if (url.isNotEmpty()) {
240- callback.invoke(
241- newExtractorLink(
242- source = " Youtube $langString " ,
243- name = " Youtube $langString " ,
244- url = url,
245- type = ExtractorLinkType .M3U8
246- ) {
247- this .referer = " ${mainUrl} /"
248- this .quality = height
249- }
250- )
251- }
252- }
253- variantIndex++
254- }
255- }
256- }
257- }
258- } catch (e: Exception ) {
259- e.printStackTrace()
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+ for (caption in response.captions.playerCaptionsTracklistRenderer.captionTracks) {
157+ subtitleCallback.invoke(
158+ newSubtitleFile(
159+ caption.name.simpleText,
160+ " ${caption.baseUrl} &fmt=ttml" // The default format is not supported
161+ ) { headers = HEADERS })
260162 }
261- }
262- }
263163
264- /*
265- Subtitle Data Class
164+ val hlsUrl = response.streamingData.hlsManifestUrl
165+ val getHls = app.get(hlsUrl, headers = HEADERS ).text
166+ val playlist = HlsPlaylistParser .parse(hlsUrl, getHls) ? : return
167+
168+ var variantIndex = 0
169+ for (tag in playlist.tags) {
170+ val trimmedTag = tag.trim()
171+ if (! trimmedTag.startsWith(" #EXT-X-STREAM-INF" )) {
172+ continue
173+ }
174+ val variant = playlist.variants.getOrNull(variantIndex++ ) ? : continue
266175
267- data class Captions(
268- val captions: PlayerCaptions
269- )
176+ val audioId = trimmedTag.split(" ," )
177+ .find { it.trim().startsWith(" YT-EXT-AUDIO-CONTENT-ID=" ) }
178+ ?.split(" =" )
179+ ?.get(1 )
180+ ?.trim(' "' ) ? : " "
270181
271- data class PlayerCaptions(
272- val playerCaptionsTracklistRenderer: CaptionsTracklistRenderer
273- )
182+ val langString =
183+ SubtitleHelper .fromTagToEnglishLanguageName(
184+ audioId.substringBefore(" ." )
185+ ) ? : SubtitleHelper .fromTagToEnglishLanguageName(
186+ audioId.substringBefore(" -" )
187+ ) ? : audioId
274188
275- data class CaptionsTracklistRenderer(
276- val captionTracks: List<CaptionTrack>?
277- )
189+ val url = variant.url.toString()
278190
279- data class CaptionTrack(
280- val baseUrl: String?,
281- val name: LanguageName?,
282- val languageCode: String?
283- )
191+ if (url.isBlank()) {
192+ continue
193+ }
284194
285- data class LanguageName(
286- val simpleText: String?
287- )
288- */
195+ callback.invoke(
196+ newExtractorLink(
197+ source = this .name,
198+ name = " Youtube${if (langString.isNotBlank()) " $langString " else " " } " ,
199+ url = url,
200+ type = ExtractorLinkType .M3U8
201+ ) {
202+ this .referer = " ${mainUrl} /"
203+ this .quality = variant.format.height
204+ }
205+ )
206+ }
207+ }
208+
209+
210+ private data class Root (
211+ // val responseContext: ResponseContext,
212+ // val playabilityStatus: PlayabilityStatus,
213+ @JsonProperty(" streamingData" )
214+ val streamingData : StreamingData ,
215+ // val playbackTracking: PlaybackTracking,
216+ @JsonProperty(" captions" )
217+ val captions : Captions ,
218+ // val videoDetails: VideoDetails,
219+ // val annotations: List<Annotation>,
220+ // val playerConfig: PlayerConfig,
221+ // val storyboards: Storyboards,
222+ // val microformat: Microformat,
223+ // val cards: Cards,
224+ // val trackingParams: String,
225+ // val endscreen: Endscreen,
226+ // val paidContentOverlay: PaidContentOverlay,
227+ // val adPlacements: List<AdPlacement>,
228+ // val adBreakHeartbeatParams: String,
229+ // val frameworkUpdates: FrameworkUpdates,
230+ )
231+
232+ private data class StreamingData (
233+ // val expiresInSeconds: String,
234+ // val formats: List<Format>,
235+ // val adaptiveFormats: List<AdaptiveFormat>,
236+ @JsonProperty(" hlsManifestUrl" )
237+ val hlsManifestUrl : String ,
238+ // val serverAbrStreamingUrl: String,
239+ )
240+
241+ private data class Captions (
242+ @JsonProperty(" playerCaptionsTracklistRenderer" )
243+ val playerCaptionsTracklistRenderer : PlayerCaptionsTracklistRenderer ,
244+ )
245+
246+ private data class PlayerCaptionsTracklistRenderer (
247+ @JsonProperty(" captionTracks" )
248+ val captionTracks : List <CaptionTrack >,
249+ // val audioTracks: List<AudioTrack>,
250+ // val translationLanguages: List<TranslationLanguage>,
251+ // @JsonProperty("defaultAudioTrackIndex")
252+ // val defaultAudioTrackIndex: Long,
253+ )
254+
255+ private data class CaptionTrack (
256+ @JsonProperty(" baseUrl" )
257+ val baseUrl : String ,
258+ @JsonProperty(" name" )
259+ val name : Name ,
260+ // val vssId: String,
261+ // val languageCode: String,
262+ // val kind: String?,
263+ // val isTranslatable: Boolean,
264+ // val trackName: String,
265+ )
266+
267+ private data class Name (
268+ @JsonProperty(" simpleText" )
269+ val simpleText : String ,
270+ )
271+
272+ // data class AudioTrack(
273+ // val captionTrackIndices: List<Long>,
274+ // val defaultCaptionTrackIndex: Long,
275+ // val hasDefaultTrack: Boolean,
276+ // val audioTrackId: String,
277+ // val captionsInitialState: String,
278+ // )
279+ }
0 commit comments