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 82e2959f2c2..ffc1666a99a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -1,26 +1,26 @@ +// Made For cs-kraptor By @trup40, @kraptor123, @ByAyzen package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.newSubtitleFile import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.INFER_TYPE -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.HlsPlaylistParser +import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.newExtractorLink -import com.lagradost.cloudstream3.utils.schemaStripRegex -import org.schabi.newpipe.extractor.ServiceList -import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor -import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory -import org.schabi.newpipe.extractor.stream.SubtitlesStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.net.URLDecoder + class YoutubeShortLinkExtractor : YoutubeExtractor() { override val mainUrl = "https://youtu.be" - - override fun getExtractorUrl(id: String): String { - return "$mainUrl/$id" - } } class YoutubeMobileExtractor : YoutubeExtractor() { @@ -31,72 +31,258 @@ class YoutubeNoCookieExtractor : YoutubeExtractor() { override val mainUrl = "https://www.youtube-nocookie.com" } + 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 var ytVideos: MutableMap = mutableMapOf() - private var ytVideosSubtitles: MutableMap> = mutableMapOf() + private 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): JSONObject? { + try { + val regex = Regex("""ytcfg\.set\(\s*(\{.*?\})\s*\)\s*;""") + val match = regex.find(html) + if (match != null) { + return JSONObject(match.groupValues[1]) + } + } catch (e: Exception) { + logError(e) + } + return null } - override fun getExtractorUrl(id: String): String { - return "$mainUrl/watch?v=$id" + private suspend fun getPageConfig(videoId: String? = null): Map? = + withContext(Dispatchers.IO) { + try { + val url = if (videoId != null) "$mainUrl/watch?v=$videoId" else mainUrl + val response = app.get(url, headers = HEADERS) + val html = response.text + val ytCfg = extractYtCfg(html) ?: return@withContext null + + val apiKey = ytCfg.optString("INNERTUBE_API_KEY") + val clientVersion = ytCfg.optString("INNERTUBE_CLIENT_VERSION", "2.20240725.01.00") + val visitorData = ytCfg.optString("VISITOR_DATA", "") + + if (apiKey.isNotEmpty()) { + return@withContext mapOf( + "apiKey" to apiKey, + "clientVersion" to clientVersion, + "visitorData" to visitorData + ) + } + } catch (e: Exception) { + logError(e) + } + return@withContext null + } + + 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 suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - if (ytVideos[url].isNullOrEmpty()) { - val link = - YoutubeStreamLinkHandlerFactory.getInstance().fromUrl( - url.replace( - schemaStripRegex, "" - ) - ) + val videoId = extractYouTubeId(url) - val s = object : YoutubeStreamExtractor( - ServiceList.YouTube, - link - ) { + val config = getPageConfig(videoId) ?: return - } - s.fetchPage() - val streamUrl = s.hlsUrl.takeIf { !it.isNullOrEmpty() } - ?: s.dashMpdUrl.takeIf { !it.isNullOrEmpty() } - ?: s.videoStreams?.firstOrNull()?.content + val apiKey = config["apiKey"] + val clientVersion = config["clientVersion"] + val visitorData = config["visitorData"] - if (!streamUrl.isNullOrEmpty()) { - ytVideos[url] = streamUrl - } + val apiUrl = "$youtubeUrl/youtubei/v1/player?key=$apiKey" - ytVideosSubtitles[url] = try { - s.subtitlesDefault.filterNotNull() - } catch (e: Exception) { - logError(e) - emptyList() + val jsonBody = """ + { + "context": { + "client": { + "hl": "en", + "gl": "US", + "clientName": "WEB", + "clientVersion": "$clientVersion", + "visitorData": "$visitorData", + "platform": "DESKTOP", + "userAgent": "$USER_AGENT" + } + }, + "videoId": "$videoId", + "playbackContext": { + "contentPlaybackContext": { + "html5Preference": "HTML5_PREF_WANTS" + } } } - ytVideos[url]?.let { - callback( - newExtractorLink( - source = this.name, - name = this.name, - url = it, - type = INFER_TYPE - ) - ) - } + """ + + try { + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = jsonBody.toRequestBody(mediaType) + + val response = app.post(apiUrl, headers = HEADERS, requestBody = requestBody) + val jsonResponse = JSONObject(response.text) + + /* + Subtitles Not Working Help Wanted + + val subtitles = mapper.readValue(response.text) + + subtitles.captions.playerCaptionsTracklistRenderer.captionTracks?.forEach { subtitle -> + val url = subtitle.baseUrl ?: "" + val lang = subtitle.name?.simpleText ?: "" + subtitleCallback.invoke(newSubtitleFile( + lang = lang, url = url + ) { + this.headers = HEADERS + }) + } + + */ + + val streamingData = jsonResponse.optJSONObject("streamingData") + + if (streamingData != null) { + val hlsUrl = streamingData.optString("hlsManifestUrl") + val getHls = app.get(hlsUrl, HEADERS).text + + val playlist = HlsPlaylistParser.parse(hlsUrl, getHls) - ytVideosSubtitles[url]?.mapNotNull { - newSubtitleFile( - it.languageTag ?: return@mapNotNull null, - it.content ?: return@mapNotNull null - ) - }?.forEach(subtitleCallback) + playlist?.let { playL -> + var variantIndex = 0 + + playL.tags.forEach { tag -> + val trimmedTag = tag.trim() + + if (trimmedTag.startsWith("#EXT-X-STREAM-INF")) { + + if (variantIndex < playL.variants.size) { + val variant = playL.variants[variantIndex] + + val audioId = trimmedTag.split(",") + .find { it.trim().startsWith("YT-EXT-AUDIO-CONTENT-ID=") } + ?.split("=") + ?.get(1) + ?.trim('"') + + val langString = if (!audioId.isNullOrEmpty()) { + val lang = audioId.substringBefore(".") + SubtitleHelper.fromTagToEnglishLanguageName(lang) + } else { + "" + } + + val height = variant.format.height + + val url = variant.url.toString() + + if (url.isNotEmpty()) { + callback.invoke( + newExtractorLink( + source = "Youtube $langString", + name = "Youtube $langString", + url = url, + type = ExtractorLinkType.M3U8 + ) { + this.referer = "${mainUrl}/" + this.quality = height + } + ) + } + } + variantIndex++ + } + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } } } + +/* +Subtitle Data Class + +data class Captions( + val captions: PlayerCaptions +) + +data class PlayerCaptions( + val playerCaptionsTracklistRenderer: CaptionsTracklistRenderer +) + +data class CaptionsTracklistRenderer( + val captionTracks: List? +) + +data class CaptionTrack( + val baseUrl: String?, + val name: LanguageName?, + val languageCode: String? +) + +data class LanguageName( + val simpleText: String? +) +*/ \ No newline at end of file