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 97ae441837..4617aed7f8 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
@@ -13,6 +15,7 @@ import com.github.libretube.api.obj.Subtitle
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
@@ -57,6 +60,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/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
index 40b11e70e3..a31733c400 100644
--- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
+++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
@@ -372,7 +372,7 @@ object PlayerHelper {
val useHlsOverDash: Boolean
get() = PreferenceHelper.getBoolean(
PreferenceKeys.USE_HLS_OVER_DASH,
- true
+ false
)
var repeatMode: Int
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 426a623599..f948fd842d 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" }