Skip to content

Commit 2b7ff8b

Browse files
authored
Fix: YT cleanup and subtitle fix
1 parent 2aaf99b commit 2b7ff8b

File tree

1 file changed

+147
-156
lines changed

1 file changed

+147
-156
lines changed
Lines changed: 147 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
// Made For cs-kraptor By @trup40, @kraptor123, @ByAyzen
22
package com.lagradost.cloudstream3.extractors
33

4+
import com.fasterxml.jackson.annotation.JsonProperty
45
import com.lagradost.cloudstream3.SubtitleFile
56
import 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
79
import com.lagradost.cloudstream3.utils.ExtractorApi
810
import com.lagradost.cloudstream3.utils.ExtractorLink
911
import com.lagradost.cloudstream3.utils.ExtractorLinkType
1012
import com.lagradost.cloudstream3.utils.HlsPlaylistParser
1113
import com.lagradost.cloudstream3.utils.SubtitleHelper
1214
import com.lagradost.cloudstream3.utils.newExtractorLink
13-
import kotlinx.coroutines.Dispatchers
14-
import kotlinx.coroutines.IO
15-
import kotlinx.coroutines.withContext
1615
import okhttp3.MediaType.Companion.toMediaType
1716
import okhttp3.RequestBody.Companion.toRequestBody
18-
import org.json.JSONObject
1917
import java.net.URLDecoder
2018

2119

@@ -31,15 +29,14 @@ class YoutubeNoCookieExtractor : YoutubeExtractor() {
3129
override val mainUrl = "https://www.youtube-nocookie.com"
3230
}
3331

34-
3532
open 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

Comments
 (0)