From 2229c6e0cd4d05951f550637243f49211596c2dd Mon Sep 17 00:00:00 2001 From: FineFindus Date: Mon, 3 Feb 2025 20:56:21 +0100 Subject: [PATCH] feat(StreamsExtractor): generate PoToken Implements support for locally generating PoTokens using the device webview. This is a direct port of https://github.com/TeamNewPipe/NewPipe/pull/11955 to native Kotlin. Closes: https://github.com/libre-tube/LibreTube/issues/7065 --- app/build.gradle.kts | 1 + app/src/main/assets/po_token.html | 127 ++++++++ .../com/github/libretube/api/ExternalApi.kt | 17 ++ .../libretube/api/local/JavaScriptUtil.kt | 133 +++++++++ .../libretube/api/local/PoTokenGenerator.kt | 123 ++++++++ .../libretube/api/local/PoTokenWebView.kt | 277 ++++++++++++++++++ .../api/{ => local}/StreamsExtractor.kt | 9 +- .../libretube/repo/LocalFeedRepository.kt | 2 +- .../repo/LocalPlaylistsRepository.kt | 2 +- .../libretube/services/DownloadService.kt | 2 +- .../libretube/services/OnlinePlayerService.kt | 2 +- .../PlaylistDownloadEnqueueService.kt | 2 +- .../ui/activities/AddToPlaylistActivity.kt | 2 +- .../libretube/ui/dialogs/DownloadDialog.kt | 2 +- .../com/github/libretube/util/PlayingQueue.kt | 2 +- gradle/libs.versions.toml | 4 +- 16 files changed, 696 insertions(+), 11 deletions(-) create mode 100644 app/src/main/assets/po_token.html create mode 100644 app/src/main/java/com/github/libretube/api/local/JavaScriptUtil.kt create mode 100644 app/src/main/java/com/github/libretube/api/local/PoTokenGenerator.kt create mode 100644 app/src/main/java/com/github/libretube/api/local/PoTokenWebView.kt rename app/src/main/java/com/github/libretube/api/{ => local}/StreamsExtractor.kt (95%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a8630b6ca6..cbcc6b86fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -133,6 +133,7 @@ dependencies { /* NewPipe Extractor */ implementation(libs.newpipeextractor) + /* Coil */ coreLibraryDesugaring(libs.desugaring) implementation(libs.coil) diff --git a/app/src/main/assets/po_token.html b/app/src/main/assets/po_token.html new file mode 100644 index 0000000000..0dc4af289a --- /dev/null +++ b/app/src/main/assets/po_token.html @@ -0,0 +1,127 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/ExternalApi.kt b/app/src/main/java/com/github/libretube/api/ExternalApi.kt index b8107dd466..5f6de3b6d6 100644 --- a/app/src/main/java/com/github/libretube/api/ExternalApi.kt +++ b/app/src/main/java/com/github/libretube/api/ExternalApi.kt @@ -5,8 +5,10 @@ import com.github.libretube.api.obj.PipedInstance import com.github.libretube.api.obj.SubmitSegmentResponse import com.github.libretube.api.obj.VoteInfo import com.github.libretube.obj.update.UpdateInfo +import kotlinx.serialization.json.JsonElement import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.Headers import retrofit2.http.POST import retrofit2.http.Query import retrofit2.http.Url @@ -14,6 +16,8 @@ import retrofit2.http.Url private const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/releases/latest" private const val SB_API_URL = "https://sponsor.ajay.app" private const val RYD_API_URL = "https://returnyoutubedislikeapi.com" +private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" +const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3" interface ExternalApi { // only for fetching servers list @@ -51,4 +55,17 @@ interface ExternalApi { @Query("userID") userID: String, @Query("type") score: Int ) + + @Headers( + "User-Agent: $USER_AGENT", + "Accept: application/json", + "Content-Type: application/json+protobuf", + "x-goog-api-key: $GOOGLE_API_KEY", + "x-user-agent: grpc-web-javascript/0.1", + ) + @POST + suspend fun botguardRequest( + @Url url: String, + @Body jsonPayload: List + ): JsonElement } diff --git a/app/src/main/java/com/github/libretube/api/local/JavaScriptUtil.kt b/app/src/main/java/com/github/libretube/api/local/JavaScriptUtil.kt new file mode 100644 index 0000000000..df14e0d944 --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/local/JavaScriptUtil.kt @@ -0,0 +1,133 @@ +package com.github.libretube.api.local + +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long + +/** + * Parses the raw challenge data obtained from the Create endpoint and returns an object that can be + * embedded in a JavaScript snippet. + */ +fun parseChallengeData(rawChallengeData: String): String { + val scrambled = Json.parseToJsonElement(rawChallengeData).jsonArray + + val challengeData = if (scrambled.size > 1 && scrambled[1].jsonPrimitive.isString) { + val descrambled = descramble(scrambled[1].jsonPrimitive.content) + Json.parseToJsonElement(descrambled).jsonArray + } else { + scrambled[1].jsonArray + } + + val messageId = challengeData[0].jsonPrimitive.content + val interpreterHash = challengeData[3].jsonPrimitive.content + val program = challengeData[4].jsonPrimitive.content + val globalName = challengeData[5].jsonPrimitive.content + val clientExperimentsStateBlob = challengeData[7].jsonPrimitive.content + + + val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData[1] + .takeIf { it !is JsonNull } + ?.jsonArray + ?.find { it.jsonPrimitive.isString } + + val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData[2] + .takeIf { it !is JsonNull } + ?.jsonArray + ?.find { it.jsonPrimitive.isString } + + + return Json.encodeToString( + JsonObject.serializer(), JsonObject( + mapOf( + "messageId" to JsonPrimitive(messageId), + "interpreterJavascript" to JsonObject( + mapOf( + "privateDoNotAccessOrElseSafeScriptWrappedValue" to (privateDoNotAccessOrElseSafeScriptWrappedValue + ?: JsonPrimitive("")), + "privateDoNotAccessOrElseTrustedResourceUrlWrappedValue" to (privateDoNotAccessOrElseTrustedResourceUrlWrappedValue + ?: JsonPrimitive("")) + ) + ), + "interpreterHash" to JsonPrimitive(interpreterHash), + "program" to JsonPrimitive(program), + "globalName" to JsonPrimitive(globalName), + "clientExperimentsStateBlob" to JsonPrimitive(clientExperimentsStateBlob) + ) + ) + ) +} + +/** + * Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript + * `Uint8Array` that can be embedded directly in JavaScript code, and an [Int] representing the + * duration of this token in seconds. + */ +fun parseIntegrityTokenData(rawIntegrityTokenData: String): Pair { + val integrityTokenData = Json.parseToJsonElement(rawIntegrityTokenData).jsonArray + return base64ToU8(integrityTokenData[0].jsonPrimitive.content) to integrityTokenData[1].jsonPrimitive.long +} + +/** + * Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript + * `Uint8Array` that can be embedded directly in JavaScript code. + */ +fun stringToU8(identifier: String): String { + return newUint8Array(identifier.toByteArray()) +} + +/** + * Takes a poToken encoded as a sequence of bytes represented as integers separated by commas + * (e.g. "97,98,99" would be "abc"), which is the output of `Uint8Array::toString()` in JavaScript, + * and converts it to the specific base64 representation for poTokens. + */ +fun u8ToBase64(poToken: String): String { + return poToken.split(",") + .map { it.toUByte().toByte() } + .toByteArray() + .toByteString() + .base64() + .replace("+", "-") + .replace("/", "_") +} + +/** + * Takes the scrambled challenge, decodes it from base64, adds 97 to each byte. + */ +private fun descramble(scrambledChallenge: String): String { + return base64ToByteString(scrambledChallenge) + .map { (it + 97).toByte() } + .toByteArray() + .decodeToString() +} + +/** + * Decodes a base64 string encoded in the specific base64 representation used by YouTube, and + * returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code. + */ +private fun base64ToU8(base64: String): String { + return newUint8Array(base64ToByteString(base64)) +} + +private fun newUint8Array(contents: ByteArray): String { + return "new Uint8Array([" + contents.joinToString(separator = ",") { it.toUByte().toString() } + "])" +} + +/** + * Decodes a base64 string encoded in the specific base64 representation used by YouTube. + */ +private fun base64ToByteString(base64: String): ByteArray { + val base64Mod = base64 + .replace('-', '+') + .replace('_', '/') + .replace('.', '=') + + return (base64Mod.decodeBase64() ?: throw PoTokenException("Cannot base64 decode")) + .toByteArray() +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/local/PoTokenGenerator.kt b/app/src/main/java/com/github/libretube/api/local/PoTokenGenerator.kt new file mode 100644 index 0000000000..ddd84b3a49 --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/local/PoTokenGenerator.kt @@ -0,0 +1,123 @@ +package com.github.libretube.api.local + +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.CookieManager +import com.github.libretube.BuildConfig +import com.github.libretube.LibreTubeApp +import kotlinx.coroutines.runBlocking +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo +import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider +import org.schabi.newpipe.extractor.services.youtube.PoTokenResult +import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper + +class PoTokenGenerator : PoTokenProvider { + val TAG = PoTokenGenerator::class.simpleName + private val supportsWebView by lazy { runCatching { CookieManager.getInstance() }.isSuccess } + + private object WebPoTokenGenLock + private var webPoTokenVisitorData: String? = null + private var webPoTokenStreamingPot: String? = null + private var webPoTokenGenerator: PoTokenWebView? = null + + + override fun getWebClientPoToken(videoId: String): PoTokenResult? { + if (!supportsWebView) { + return null + } + + return getWebClientPoToken(videoId, false) + } + + /** + * @param forceRecreate whether to force the recreation of [webPoTokenGenerator], to be used in + * case the current [webPoTokenGenerator] threw an error last time + * [PoTokenGenerator.generatePoToken] was called + */ + private fun getWebClientPoToken(videoId: String, forceRecreate: Boolean): PoTokenResult { + // just a helper class since Kotlin does not have builtin support for 4-tuples + data class Quadruple(val t1: T1, val t2: T2, val t3: T3, val t4: T4) + + val (poTokenGenerator, visitorData, streamingPot, hasBeenRecreated) = + synchronized(WebPoTokenGenLock) { + val shouldRecreate = webPoTokenGenerator == null || forceRecreate || webPoTokenGenerator!!.isExpired() + + if (shouldRecreate) { + val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient() + innertubeClientRequestInfo.clientInfo.clientVersion = + YoutubeParsingHelper.getClientVersion() + + webPoTokenVisitorData = YoutubeParsingHelper.getVisitorDataFromInnertube( + innertubeClientRequestInfo, + NewPipe.getPreferredLocalization(), + NewPipe.getPreferredContentCountry(), + YoutubeParsingHelper.getYouTubeHeaders(), + YoutubeParsingHelper.YOUTUBEI_V1_URL, + null, + false + ) + + runBlocking { + // close the current webPoTokenGenerator on the main thread + webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } } + + // create a new webPoTokenGenerator + webPoTokenGenerator = PoTokenWebView + .newPoTokenGenerator(LibreTubeApp.instance) + + // The streaming poToken needs to be generated exactly once before generating + // any other (player) tokens. + webPoTokenStreamingPot = webPoTokenGenerator!!.generatePoToken(webPoTokenVisitorData!!) + } + } + + return@synchronized Quadruple( + webPoTokenGenerator!!, + webPoTokenVisitorData!!, + webPoTokenStreamingPot!!, + shouldRecreate + ) + } + + val playerPot = try { + // Not using synchronized here, since poTokenGenerator would be able to generate + // multiple poTokens in parallel if needed. The only important thing is for exactly one + // visitorData/streaming poToken to be generated before anything else. + runBlocking { + poTokenGenerator.generatePoToken(videoId) + } + } catch (throwable: Throwable) { + if (hasBeenRecreated) { + // the poTokenGenerator has just been recreated (and possibly this is already the + // second time we try), so there is likely nothing we can do + throw throwable + } else { + // retry, this time recreating the [webPoTokenGenerator] from scratch; + // this might happen for example if NewPipe goes in the background and the WebView + // content is lost + Log.e(TAG, "Failed to obtain poToken, retrying", throwable) + return getWebClientPoToken(videoId = videoId, forceRecreate = true) + } + } + + + if (BuildConfig.DEBUG) { + Log.d( + TAG, + "poToken for $videoId: playerPot=$playerPot, " + + "streamingPot=$streamingPot, visitor_data=$visitorData" + ) + } + + return PoTokenResult(visitorData, playerPot, streamingPot) + } + + override fun getWebEmbedClientPoToken(videoId: String?): PoTokenResult? = null + + override fun getAndroidClientPoToken(videoId: String?): PoTokenResult? = null + + override fun getIosClientPoToken(videoId: String?): PoTokenResult? = null +} + diff --git a/app/src/main/java/com/github/libretube/api/local/PoTokenWebView.kt b/app/src/main/java/com/github/libretube/api/local/PoTokenWebView.kt new file mode 100644 index 0000000000..8efd635692 --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/local/PoTokenWebView.kt @@ -0,0 +1,277 @@ +package com.github.libretube.api.local + +import android.content.Context +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.annotation.MainThread +import com.github.libretube.BuildConfig +import com.github.libretube.api.RetrofitInstance +import com.github.libretube.api.USER_AGENT +import kotlinx.coroutines.* +import java.time.Instant +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class PoTokenWebView private constructor( + context: Context, + private val generatorContinuation: Continuation +) { + private val webView = WebView(context) + private val poTokenContinuations = mutableMapOf>() + private lateinit var expirationInstant: Instant + + //region Initialization + init { + val webViewSettings = webView.settings + //noinspection SetJavaScriptEnabled we want to use JavaScript! + webViewSettings.javaScriptEnabled = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + webViewSettings.safeBrowsingEnabled = false + } + webViewSettings.userAgentString = USER_AGENT + webViewSettings.blockNetworkLoads = true // the WebView does not need internet access + + // so that we can run async functions and get back the result + webView.addJavascriptInterface(this, JS_INTERFACE) + } + + /** + * Must be called right after instantiating [PoTokenWebView] to perform the actual + * initialization. This will asynchronously go through all the steps needed to load BotGuard, + * run it, and obtain an `integrityToken`. + */ + private fun loadHtmlAndObtainBotguard(context: Context) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "loadHtmlAndObtainBotguard() called") + } + + CoroutineScope(Dispatchers.IO).launch { + try { + val html = context.assets.open("po_token.html").bufferedReader().use { it.readText() } + withContext(Dispatchers.Main) { + webView.loadDataWithBaseURL( + "https://www.youtube.com", + html.replaceFirst( + "", + // calls downloadAndRunBotguard() when the page has finished loading + "\n$JS_INTERFACE.downloadAndRunBotguard()" + ), + "text/html", + "utf-8", + null, + ) + } + } catch (e: Exception) { + onInitializationError(e) + } + } + } + + /** + * Called during initialization by the JavaScript snippet appended to the HTML page content in + * [loadHtmlAndObtainBotguard] after the WebView content has been loaded. + */ + @JavascriptInterface + fun downloadAndRunBotguard() { + if (BuildConfig.DEBUG) { + Log.d(TAG, "downloadAndRunBotguard() called") + } + + CoroutineScope(Dispatchers.IO).launch { + val responseBody = makeBotguardServiceRequest( + "https://www.youtube.com/api/jnn/v1/Create", + listOf(REQUEST_KEY) + ) + val parsedChallengeData = parseChallengeData(responseBody) + withContext(Dispatchers.Main) { + webView.evaluateJavascript( + """try { + data = $parsedChallengeData + runBotGuard(data).then(function (result) { + this.webPoSignalOutput = result.webPoSignalOutput + $JS_INTERFACE.onRunBotguardResult(result.botguardResponse) + }, function (error) { + $JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack) + }) + } catch (error) { + $JS_INTERFACE.onJsInitializationError(error + "\n" + error.stack) + }""", + null + ) + } + } + } + + /** + * Called during initialization by the JavaScript snippets from either + * [downloadAndRunBotguard] or [onRunBotguardResult]. + */ + @JavascriptInterface + fun onJsInitializationError(error: String) { + if (BuildConfig.DEBUG) { + Log.e(TAG, "Initialization error from JavaScript: $error") + } + onInitializationError(PoTokenException(error)) + } + + /** + * Called during initialization by the JavaScript snippet from [downloadAndRunBotguard] after + * obtaining the BotGuard execution output [botguardResponse]. + */ + @JavascriptInterface + fun onRunBotguardResult(botguardResponse: String) { + CoroutineScope(Dispatchers.IO).launch { + val response = makeBotguardServiceRequest( + "https://www.youtube.com/api/jnn/v1/GenerateIT", + listOf(REQUEST_KEY, botguardResponse) + ) + val (integrityToken, expirationTimeInSeconds) = parseIntegrityTokenData(response) + + // leave 10 minutes of margin just to be sure + expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds - 600) + + withContext(Dispatchers.Main) { + webView.evaluateJavascript( + "this.integrityToken = $integrityToken" + ) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "initialization finished, expiration=${expirationTimeInSeconds}s") + } + generatorContinuation.resume(this@PoTokenWebView) + } + } + } + } + //endregion + + //region Obtaining poTokens + suspend fun generatePoToken(identifier: String): String { + if (BuildConfig.DEBUG) { + Log.d(TAG, "generatePoToken() called with identifier $identifier") + } + return suspendCancellableCoroutine { continuation -> + poTokenContinuations[identifier] = continuation + val u8Identifier = stringToU8(identifier) + + Handler(Looper.getMainLooper()).post { + webView.evaluateJavascript( + """try { + identifier = "$identifier" + u8Identifier = $u8Identifier + poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier) + poTokenU8String = "" + for (i = 0; i < poTokenU8.length; i++) { + if (i != 0) poTokenU8String += "," + poTokenU8String += poTokenU8[i] + } + $JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String) + } catch (error) { + $JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack) + }""", + ) {} + } + } + } + + /** + * Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the + * JavaScript `obtainPoToken()` function. + */ + @JavascriptInterface + fun onObtainPoTokenError(identifier: String, error: String) { + if (BuildConfig.DEBUG) { + Log.e(TAG, "obtainPoToken error from JavaScript: $error") + } + poTokenContinuations.remove(identifier)?.resumeWithException(PoTokenException(error)) + } + + /** + * Called by the JavaScript snippet from [generatePoToken] with the original identifier and the + * result of the JavaScript `obtainPoToken()` function. + */ + @JavascriptInterface + fun onObtainPoTokenResult(identifier: String, poTokenU8: String) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Generated poToken (before decoding): identifier=$identifier poTokenU8=$poTokenU8") + } + val poToken = try { + u8ToBase64(poTokenU8) + } catch (t: Throwable) { + poTokenContinuations.remove(identifier)?.resumeWithException(t) + return + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Generated poToken: identifier=$identifier poToken=$poToken") + } + poTokenContinuations.remove(identifier)?.resume(poToken) + } + + fun isExpired(): Boolean { + return Instant.now().isAfter(expirationInstant) + } + //endregion + + //region Utils + /** + * Makes a POST request to [url] with the given [data] by setting the correct headers. + * This is supposed to be used only during initialization. Returns the response body + * as a String if the response is successful. + */ + private suspend fun makeBotguardServiceRequest(url: String, data: List): String = withContext(Dispatchers.IO) { + val response = RetrofitInstance.externalApi.botguardRequest(url, data) + response.toString() + } + + /** + * Handles any error happening during initialization, releasing resources and sending the error + * to [generatorContinuation]. + */ + private fun onInitializationError(error: Throwable) { + CoroutineScope(Dispatchers.Main).launch { + close() + generatorContinuation.resumeWithException(error) + } + } + + /** + * Releases all [webView] resources. + */ + @MainThread + fun close() { + webView.clearHistory() + // clears RAM cache and disk cache (globally for all WebViews) + webView.clearCache(true) + + // ensures that the WebView isn't doing anything when destroying it + webView.loadUrl("about:blank") + + webView.onPause() + webView.removeAllViews() + webView.destroy() + } + //endregion + + companion object { + private val TAG = PoTokenWebView::class.simpleName + private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo" + private const val JS_INTERFACE = "PoTokenWebView" + + suspend fun newPoTokenGenerator(context: Context): PoTokenWebView { + return suspendCancellableCoroutine { continuation -> + Handler(Looper.getMainLooper()).post { + val poTokenWebView = PoTokenWebView(context, continuation) + poTokenWebView.loadHtmlAndObtainBotguard(context) + } + } + } + } +} + + +class PoTokenException(message: String) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt b/app/src/main/java/com/github/libretube/api/local/StreamsExtractor.kt similarity index 95% rename from app/src/main/java/com/github/libretube/api/StreamsExtractor.kt rename to app/src/main/java/com/github/libretube/api/local/StreamsExtractor.kt index 5320de2688..f48bd6572d 100644 --- a/app/src/main/java/com/github/libretube/api/StreamsExtractor.kt +++ b/app/src/main/java/com/github/libretube/api/local/StreamsExtractor.kt @@ -1,7 +1,9 @@ -package com.github.libretube.api +package com.github.libretube.api.local import android.content.Context import com.github.libretube.R +import com.github.libretube.api.JsonHelper +import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.obj.ChapterSegment import com.github.libretube.api.obj.Message import com.github.libretube.api.obj.MetaInfo @@ -14,6 +16,7 @@ import com.github.libretube.extensions.toID import com.github.libretube.helpers.PlayerHelper import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL import kotlinx.datetime.toKotlinInstant +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.VideoStream @@ -58,6 +61,10 @@ fun StreamInfoItem.toStreamItem( ) object StreamsExtractor { + init { + YoutubeStreamExtractor.setPoTokenProvider(PoTokenGenerator()); + } + suspend fun extractStreams(videoId: String): Streams { if (!PlayerHelper.disablePipedProxy || !PlayerHelper.localStreamExtraction) { return RetrofitInstance.api.getStreams(videoId) diff --git a/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt index 5bf8679f8f..ed15ba82ae 100644 --- a/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt @@ -3,7 +3,7 @@ package com.github.libretube.repo import android.util.Log import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.obj.StreamItem -import com.github.libretube.api.toStreamItem +import com.github.libretube.api.local.toStreamItem import com.github.libretube.constants.PreferenceKeys import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.SubscriptionsFeedItem diff --git a/app/src/main/java/com/github/libretube/repo/LocalPlaylistsRepository.kt b/app/src/main/java/com/github/libretube/repo/LocalPlaylistsRepository.kt index 63c35c9c5d..9df96c4679 100644 --- a/app/src/main/java/com/github/libretube/repo/LocalPlaylistsRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/LocalPlaylistsRepository.kt @@ -3,7 +3,7 @@ package com.github.libretube.repo import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS import com.github.libretube.api.RetrofitInstance -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.StreamItem diff --git a/app/src/main/java/com/github/libretube/services/DownloadService.kt b/app/src/main/java/com/github/libretube/services/DownloadService.kt index 0428be4295..ac53c15e3b 100644 --- a/app/src/main/java/com/github/libretube/services/DownloadService.kt +++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt @@ -20,7 +20,7 @@ import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.github.libretube.LibreTubeApp.Companion.DOWNLOAD_CHANNEL_NAME import com.github.libretube.R -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.Streams import com.github.libretube.constants.IntentData import com.github.libretube.db.DatabaseHolder.Database diff --git a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt index 9f31d3dd6e..4a81fb3fe1 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -13,7 +13,7 @@ import androidx.media3.exoplayer.hls.HlsMediaSource import com.github.libretube.R import com.github.libretube.api.JsonHelper import com.github.libretube.api.RetrofitInstance -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Streams import com.github.libretube.constants.IntentData diff --git a/app/src/main/java/com/github/libretube/services/PlaylistDownloadEnqueueService.kt b/app/src/main/java/com/github/libretube/services/PlaylistDownloadEnqueueService.kt index 1cef399701..ee96f5348f 100644 --- a/app/src/main/java/com/github/libretube/services/PlaylistDownloadEnqueueService.kt +++ b/app/src/main/java/com/github/libretube/services/PlaylistDownloadEnqueueService.kt @@ -12,7 +12,7 @@ import com.github.libretube.LibreTubeApp.Companion.PLAYLIST_DOWNLOAD_ENQUEUE_CHA import com.github.libretube.R import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.RetrofitInstance -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.StreamItem import com.github.libretube.constants.IntentData diff --git a/app/src/main/java/com/github/libretube/ui/activities/AddToPlaylistActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/AddToPlaylistActivity.kt index d24037ce4c..56aa7f99f0 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/AddToPlaylistActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/AddToPlaylistActivity.kt @@ -6,7 +6,7 @@ import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import com.github.libretube.R -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.StreamItem import com.github.libretube.constants.IntentData import com.github.libretube.extensions.toastFromMainDispatcher diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt index ddd431c083..89301d8b97 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt @@ -13,7 +13,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import androidx.lifecycle.lifecycleScope import com.github.libretube.R -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Subtitle diff --git a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt index 57748de1d3..39f8ebdae2 100644 --- a/app/src/main/java/com/github/libretube/util/PlayingQueue.kt +++ b/app/src/main/java/com/github/libretube/util/PlayingQueue.kt @@ -3,7 +3,7 @@ package com.github.libretube.util import androidx.media3.common.Player import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.RetrofitInstance -import com.github.libretube.api.StreamsExtractor +import com.github.libretube.api.local.StreamsExtractor import com.github.libretube.api.obj.StreamItem import com.github.libretube.extensions.move import com.github.libretube.extensions.runCatchingIO diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d724a38fa6..4b4759b6c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ constraintlayout = "2.2.0" loggingInterceptor = "4.12.0" material = "1.12.0" navigation = "2.8.7" -newpipeextractor = "0.24.4" +newpipeextractor = "0.24.5" preference = "1.2.1" extJunit = "1.2.1" espresso = "3.6.1" @@ -58,7 +58,7 @@ androidx-media3-exoplayer-hls = { group = "androidx.media3", name="media3-exopla androidx-media3-exoplayer-dash = { group = "androidx.media3", name="media3-exoplayer-dash", version.ref="media3" } androidx-media3-session = { group="androidx.media3", name="media3-session", version.ref="media3" } androidx-media3-ui = { group="androidx.media3", name="media3-ui", version.ref="media3" } -newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version.ref = "newpipeextractor" } +newpipeextractor = { module = "com.github.TeamNewPipe:NewPipeExtractor", version.ref = "newpipeextractor" } square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" } desugaring = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugaring" }