diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b0a3e0f9..fbadbcbb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,9 +7,9 @@ android = "8.8.0" kotlin = "2.1.0" serialization = "2.1.0" ksp = "2.1.0-1.0.29" -compose-bom = "2024.12.01" +compose-bom = "2025.01.00" constraintlayout-compose = "1.1.0" -activity-compose = "1.9.3" +activity-compose = "1.10.0" lifecycle-viewmodel-compose = "2.8.7" lifecycle-runtime-ktx = "2.8.7" core-ktx = "1.15.0" @@ -30,8 +30,8 @@ androidx-junit = "1.2.1" espresso-core = "3.6.1" room = "2.6.1" legacy-support-v4 = "1.0.0" -coroutines-android = "1.9.0" -coroutines-guava = "1.9.0" +coroutines-android = "1.10.1" +coroutines-guava = "1.10.1" navigation = "2.8.5" gson = "2.10.1" coil3 = "3.0.4" @@ -39,7 +39,7 @@ kmpalette = "3.1.0" easypermissions = "3.0.0" preference-ktx = "1.2.1" fragment-ktx = "1.8.5" -datastore-preferences = "1.1.1" +datastore-preferences = "1.1.2" swiperefreshlayout = "1.2.0-alpha01" insetter = "0.6.1" shimmer = "0.5.0" @@ -145,6 +145,8 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } ksoup-html = { group = "com.mohamedrejeb.ksoup", name = "ksoup-html", version.ref = "ksoup" } ksoup-entities = { group = "com.mohamedrejeb.ksoup", name = "ksoup-entities", version.ref = "ksoup" } store = { module = "org.mobilenativefoundation.store:store5", version.ref = "store" } +# Use NewPipeExtractor from the Outertune project (Thanks to @DD3Boh) +newpipe-extractor = { group = "com.github.gechoto", name = "NewPipeExtractor", version = "00cf7dad9e49933c7faaa382c9c7b1e7afb28a7d" } #Koin koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } diff --git a/kotlinYtmusicScraper/build.gradle.kts b/kotlinYtmusicScraper/build.gradle.kts index 39d40f90..e13822c2 100644 --- a/kotlinYtmusicScraper/build.gradle.kts +++ b/kotlinYtmusicScraper/build.gradle.kts @@ -126,6 +126,7 @@ dependencies { testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.espresso.core) + coreLibraryDesugaring(libs.desugaring) implementation(libs.ktor.client.core) implementation(libs.ktor.client.cio) @@ -136,6 +137,8 @@ dependencies { implementation(libs.ktor.serialization.kotlinx.xml) implementation(libs.ktor.serialization.kotlinx.protobuf) + implementation(libs.newpipe.extractor) + implementation(libs.brotli.dec) implementation(libs.kotlin.reflect) diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt index 71f51e0a..05879d7e 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/YouTube.kt @@ -64,12 +64,15 @@ import com.maxrave.kotlinytmusicscraper.parser.getPlaylistContinuation import com.maxrave.kotlinytmusicscraper.parser.getReloadParams import com.maxrave.kotlinytmusicscraper.parser.getSuggestionSongItems import com.maxrave.kotlinytmusicscraper.parser.hasReloadParams +import com.maxrave.kotlinytmusicscraper.utils.CustomNewPipeDownloader import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser import io.ktor.client.call.body import io.ktor.client.engine.ProxyBuilder import io.ktor.client.engine.http import io.ktor.client.statement.bodyAsText +import io.ktor.http.URLBuilder +import io.ktor.http.parseQueryString import kotlinx.coroutines.delay import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive @@ -77,6 +80,9 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive import okhttp3.Interceptor import org.json.JSONArray +import org.schabi.newpipe.extractor.NewPipe +import org.schabi.newpipe.extractor.exceptions.ParsingException +import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptPlayerManager import java.io.File import kotlin.random.Random @@ -102,6 +108,7 @@ private fun List.toListFormat(): List.toListFormat(): List> = runCatching { - val (tempCookie, visitorData, playbackTracking) = getVisitorData(videoId, playlistId) val cpn = (1..16) .map { @@ -1172,96 +1186,131 @@ class YouTube { ), ] }.joinToString("") - val now = System.currentTimeMillis() - val poToken = - if (now < poTokenObject.second) { - println("Use saved PoToken") - poTokenObject.first - } else { - ytMusic - .createPoTokenChallenge() - .bodyAsText() - .let { challenge -> - val listChallenge = poTokenJsonDeserializer.decodeFromString>(challenge) - listChallenge.filterIsInstance().firstOrNull() - }?.let { poTokenChallenge -> - ytMusic.generatePoToken(poTokenChallenge).bodyAsText().getPoToken().also { poToken -> - if (poToken != null) { - poTokenObject = Pair(poToken, now + 21600000) + val sigTimestamp = getSignatureTimestamp(videoId) + val sigResponse = ytMusic.player(WEB_REMIX, videoId, playlistId, cpn, signatureTimestamp = sigTimestamp).body() + val decodedSigResponse = + sigResponse.copy( + streamingData = + sigResponse.streamingData?.copy( + formats = + sigResponse.streamingData.formats?.map { format -> + format.copy( + url = format.signatureCipher?.let { decodeSignatureCipher(videoId, it) }, + ) + }, + adaptiveFormats = + sigResponse.streamingData.adaptiveFormats.map { adaptiveFormats -> + adaptiveFormats.copy( + url = adaptiveFormats.signatureCipher?.let { decodeSignatureCipher(videoId, it) }, + ) + }, + ), + ) + val listUrlSig = + ( + decodedSigResponse.streamingData + ?.adaptiveFormats + ?.mapNotNull { it.url } + ?.toMutableList() ?: mutableListOf() + ).apply { + decodedSigResponse.streamingData + ?.formats + ?.mapNotNull { it.url } + ?.let { addAll(it) } + } + if (listUrlSig.isEmpty()) { + val (tempCookie, visitorData, playbackTracking) = getVisitorData(videoId, playlistId) + val now = System.currentTimeMillis() + val poToken = + if (now < poTokenObject.second) { + println("Use saved PoToken") + poTokenObject.first + } else { + ytMusic + .createPoTokenChallenge() + .bodyAsText() + .let { challenge -> + val listChallenge = poTokenJsonDeserializer.decodeFromString>(challenge) + listChallenge.filterIsInstance().firstOrNull() + }?.let { poTokenChallenge -> + ytMusic.generatePoToken(poTokenChallenge).bodyAsText().getPoToken().also { poToken -> + if (poToken != null) { + poTokenObject = Pair(poToken, now + 21600000) + } } } + } + println("PoToken $poToken") + val playerResponse = ytMusic.noLogInPlayer(videoId, tempCookie, visitorData, poToken ?: "").body() + println("Player Response $playerResponse") + println("Thumbnails " + playerResponse.videoDetails?.thumbnail) + println("Player Response status: ${playerResponse.playabilityStatus.status}") + val firstThumb = + playerResponse.videoDetails + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + val thumbnails = + if (firstThumb?.height == firstThumb?.width && firstThumb != null) MediaType.Song else MediaType.Video + val formatList = playerResponse.streamingData?.formats?.map { Pair(it.itag, it.isAudio) } + println("Player Response formatList $formatList") + val adaptiveFormatsList = playerResponse.streamingData?.adaptiveFormats?.map { Pair(it.itag, it.isAudio) } + println("Player Response adaptiveFormat $adaptiveFormatsList") + + if (playerResponse.playabilityStatus.status == "OK" && (formatList != null || adaptiveFormatsList != null)) { + return@runCatching Triple( + cpn, + playerResponse.copy( + videoDetails = playerResponse.videoDetails?.copy(), + playbackTracking = playbackTracking ?: playerResponse.playbackTracking, + ), + thumbnails, + ) + } else { + for (instance in listPipedInstances) { + try { + val piped = ytMusic.pipedStreams(videoId, instance).body() + val audioStreams = piped.audioStreams + val videoStreams = piped.videoStreams + val stream = audioStreams + videoStreams + return@runCatching Triple( + null, + playerResponse.copy( + streamingData = + PlayerResponse.StreamingData( + formats = stream.toListFormat(), + adaptiveFormats = stream.toListFormat(), + expiresInSeconds = 0, + ), + videoDetails = playerResponse.videoDetails?.copy(), + playbackTracking = playbackTracking ?: playerResponse.playbackTracking, + ), + thumbnails, + ) + } catch (e: Exception) { + e.printStackTrace() + continue } + } } - println("PoToken $poToken") - val playerResponse = ytMusic.noLogInPlayer(videoId, tempCookie, visitorData, poToken ?: "").body() -// try { -// println("Start logged in request") -// ytMusic.player( -// IOS, -// videoId, -// playlistId, -// cpn, -// poToken -// ).body().also { -// if (it.playabilityStatus.status != "OK") throw Exception(it.playabilityStatus.status) -// } -// } catch (e: Exception) { -// println("Player Response Error $e") -// println("Start no logged in request") -// ytMusic.noLogInPlayer(videoId, tempCookie, visitorData, poToken ?: "").body() -// } - println("Player Response $playerResponse") - println("Thumbnails " + playerResponse.videoDetails?.thumbnail) - println("Player Response status: ${playerResponse.playabilityStatus.status}") - val firstThumb = - playerResponse.videoDetails - ?.thumbnail - ?.thumbnails - ?.firstOrNull() - val thumbnails = - if (firstThumb?.height == firstThumb?.width && firstThumb != null) MediaType.Song else MediaType.Video - val formatList = playerResponse.streamingData?.formats?.map { Pair(it.itag, it.isAudio) } - println("Player Response formatList $formatList") - val adaptiveFormatsList = playerResponse.streamingData?.adaptiveFormats?.map { Pair(it.itag, it.isAudio) } - println("Player Response adaptiveFormat $adaptiveFormatsList") - - if (playerResponse.playabilityStatus.status == "OK" && (formatList != null || adaptiveFormatsList != null)) { + throw Exception(playerResponse.playabilityStatus.status ?: "Unknown error") + } else { + val firstThumb = + decodedSigResponse.videoDetails + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + val thumbnails = + if (firstThumb?.height == firstThumb?.width && firstThumb != null) MediaType.Song else MediaType.Video return@runCatching Triple( cpn, - playerResponse.copy( - videoDetails = playerResponse.videoDetails?.copy(), - playbackTracking = playbackTracking ?: playerResponse.playbackTracking, + decodedSigResponse.copy( + videoDetails = decodedSigResponse.videoDetails?.copy(), + playbackTracking = decodedSigResponse.playbackTracking, ), thumbnails, ) - } else { - for (instance in listPipedInstances) { - try { - val piped = ytMusic.pipedStreams(videoId, instance).body() - val audioStreams = piped.audioStreams - val videoStreams = piped.videoStreams - val stream = audioStreams + videoStreams - return@runCatching Triple( - null, - playerResponse.copy( - streamingData = - PlayerResponse.StreamingData( - formats = stream.toListFormat(), - adaptiveFormats = stream.toListFormat(), - expiresInSeconds = 0, - ), - videoDetails = playerResponse.videoDetails?.copy(), - playbackTracking = playbackTracking ?: playerResponse.playbackTracking, - ), - thumbnails, - ) - } catch (e: Exception) { - e.printStackTrace() - continue - } - } } - throw Exception(playerResponse.playabilityStatus.status ?: "Unknown error") } suspend fun updateWatchTime( @@ -1683,6 +1732,34 @@ class YouTube { ytMusic.removeFromLiked(mediaId).status.value } + /** + * NewPipeExtractor implement + */ + private fun getSignatureTimestamp(videoId: String): Int? = + try { + YoutubeJavaScriptPlayerManager.getSignatureTimestamp(videoId) + } catch (e: Exception) { + e.printStackTrace() + null + } + + private fun decodeSignatureCipher( + videoId: String, + signatureCipher: String, + ): String? = + try { + val params = parseQueryString(signatureCipher) + val obfuscatedSignature = params["s"] ?: throw ParsingException("Could not parse cipher signature") + val signatureParam = params["sp"] ?: throw ParsingException("Could not parse cipher signature parameter") + val url = params["url"]?.let { URLBuilder(it) } ?: throw ParsingException("Could not parse cipher url") + url.parameters[signatureParam] = YoutubeJavaScriptPlayerManager.deobfuscateSignature(videoId, obfuscatedSignature) + print("URL $url") + YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated(videoId, url.toString()) + } catch (e: Exception) { + e.printStackTrace() + null + } + companion object { const val MAX_GET_QUEUE_SIZE = 1000 diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt index 74bed027..a4ce10d7 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/Ytmusic.kt @@ -188,25 +188,27 @@ class Ytmusic { contentType(ContentType.Application.Json) } - suspend fun ghostRequest(videoId: String, playlistId: String?) = - httpClient - .get( - "https://www.youtube.com/watch?v=$videoId&bpctr=9999999999&has_verified=1" - .let { - if (playlistId != null) "$it&list=$playlistId" else it - } - ) { - headers { - header("Connection", "close") - header("Host", "www.youtube.com") - header("Cookie", if (cookie.isNullOrEmpty()) "PREF=hl=en&tz=UTC; SOCS=CAI" else cookie) - header("Sec-Fetch-Mode", "navigate") - header( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36", - ) - } + suspend fun ghostRequest( + videoId: String, + playlistId: String?, + ) = httpClient + .get( + "https://www.youtube.com/watch?v=$videoId&bpctr=9999999999&has_verified=1" + .let { + if (playlistId != null) "$it&list=$playlistId" else it + }, + ) { + headers { + header("Connection", "close") + header("Host", "www.youtube.com") + header("Cookie", if (cookie.isNullOrEmpty()) "PREF=hl=en&tz=UTC; SOCS=CAI" else cookie) + header("Sec-Fetch-Mode", "navigate") + header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36", + ) } + } private fun HttpRequestBuilder.poHeader() { headers { @@ -223,25 +225,28 @@ class Ytmusic { header("sec-fetch-site", "cross-site") header( "user-agent", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0") + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0", + ) header("x-goog-api-key", "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw") header("x-user-agent", "grpc-web-javascript/0.1") } } - suspend fun createPoTokenChallenge() = httpClient.post( - "https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/Create" - ) { - poHeader() - setBody("[\"$poTokenChallengeRequestKey\"]") - } + suspend fun createPoTokenChallenge() = + httpClient.post( + "https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/Create", + ) { + poHeader() + setBody("[\"$poTokenChallengeRequestKey\"]") + } - suspend fun generatePoToken(challenge: String) = httpClient.post( - "https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/GenerateIT" - ) { - poHeader() - setBody("[\"$poTokenChallengeRequestKey\", \"$challenge\"]") - } + suspend fun generatePoToken(challenge: String) = + httpClient.post( + "https://jnn-pa.googleapis.com/\$rpc/google.internal.waa.v1.Waa/GenerateIT", + ) { + poHeader() + setBody("[\"$poTokenChallengeRequestKey\", \"$challenge\"]") + } // curl 'https://jnn-pa.googleapis.com/$rpc/google.internal.waa.v1.Waa/Create' \ // -H 'accept: */*' \ @@ -275,7 +280,7 @@ class Ytmusic { header(HttpHeaders.UserAgent, IOS.userAgent) header( "Set-Cookie", - cookie + cookie, ) header("X-Goog-Visitor-Id", visitorData ?: this@Ytmusic.visitorData) header("X-YouTube-Client-Name", IOS.clientName) @@ -287,9 +292,10 @@ class Ytmusic { cpn = null, videoId = videoId, playbackContext = PlayerBody.PlaybackContext(), - serviceIntegrityDimensions = PlayerBody.ServiceIntegrityDimensions( - poToken = poToken, - ) + serviceIntegrityDimensions = + PlayerBody.ServiceIntegrityDimensions( + poToken = poToken, + ), ), ) parameter("prettyPrint", false) @@ -301,6 +307,7 @@ class Ytmusic { playlistId: String?, cpn: String?, poToken: String? = null, + signatureTimestamp: Int? = null, ) = httpClient.post("player") { ytClient(client, setLogin = true) setBody( @@ -321,10 +328,21 @@ class Ytmusic { videoId = videoId, playlistId = playlistId, cpn = cpn, - playbackContext = PlayerBody.PlaybackContext(), - serviceIntegrityDimensions = if (poToken != null) PlayerBody.ServiceIntegrityDimensions( - poToken = poToken - ) else null + playbackContext = + PlayerBody.PlaybackContext( + contentPlaybackContext = + PlayerBody.PlaybackContext.ContentPlaybackContext( + signatureTimestamp = signatureTimestamp ?: 20073, + ), + ), + serviceIntegrityDimensions = + if (poToken != null) { + PlayerBody.ServiceIntegrityDimensions( + poToken = poToken, + ) + } else { + null + }, ), ) } diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/PlayerResponse.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/PlayerResponse.kt index 04683961..d9a9c69c 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/PlayerResponse.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/models/response/PlayerResponse.kt @@ -63,6 +63,7 @@ data class PlayerResponse( val audioChannels: Int?, val loudnessDb: Double?, val lastModified: Long?, + val signatureCipher: String?, ) { val isAudio: Boolean get() = width == null diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt index 450b0576..27196f19 100644 --- a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/main.kt @@ -11,38 +11,11 @@ import com.maxrave.kotlinytmusicscraper.models.SectionListRenderer import com.maxrave.kotlinytmusicscraper.models.Thumbnail import com.maxrave.kotlinytmusicscraper.models.YouTubeClient import com.maxrave.kotlinytmusicscraper.models.YouTubeLocale -import io.ktor.client.call.body import io.ktor.client.statement.bodyAsText import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.Json fun main() { runBlocking { -// val ytMusic = Ytmusic() -// val jsonDeserializer = Json { -// ignoreUnknownKeys = true -// encodeDefaults = true -// coerceInputValues = true -// useArrayPolymorphism = true -// } -// ytMusic.createPoTokenChallenge().bodyAsText().let { -// jsonDeserializer.decodeFromString>(it) -// }.let { -// println(it) -// val challenge = it.getOrNull(1) -// challenge?.let { -// ytMusic.generatePoToken(challenge).bodyAsText().let { -// println(it) -// it.replace("[", "").replace("]", "") -// .split(",") -// .findLast { it.contains("\"") } -// ?.replace("\"", "") -// ?.let { -// println("Token: $it") -// } -// } -// } -// } testPlayer() } } @@ -50,22 +23,12 @@ fun main() { fun testPlayer() { runBlocking { val yt = YouTube() - yt.player( + yt + .player( "iqgYmB5vPfI", + "RDAMVMiqgYmB5vPfI", ).onSuccess { - it.second.streamingData?.let { data -> - data.adaptiveFormats.forEach { format -> - println("${format.itag} ${format.url}") - } - data.formats?.forEach { format -> - println("${format.itag} ${format.url}") - } - } - it.second.responseContext.serviceTrackingParams?.find { it.service == "GFEEDBACK" } - ?.params?.find { it.key == "logged_in" }?.let { - println("Logged in: ${it.value}") - } - println("Tracking: ${it.second.playbackTracking}") + println(it) }.onFailure { it.printStackTrace() } diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/test.txt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/test/test.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomNewPipeDownloader.kt b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomNewPipeDownloader.kt new file mode 100644 index 00000000..59665011 --- /dev/null +++ b/kotlinYtmusicScraper/src/main/java/com/maxrave/kotlinytmusicscraper/utils/CustomNewPipeDownloader.kt @@ -0,0 +1,63 @@ +package com.maxrave.kotlinytmusicscraper.utils + +import com.maxrave.kotlinytmusicscraper.models.YouTubeClient +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import org.schabi.newpipe.extractor.downloader.Downloader +import org.schabi.newpipe.extractor.downloader.Request +import org.schabi.newpipe.extractor.downloader.Response +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException +import java.io.IOException +import java.net.Proxy + +class CustomNewPipeDownloader( + proxy: Proxy?, +) : Downloader() { + fun updateProxy(proxy: Proxy?) { + client.newBuilder().proxy(proxy).build() + } + + private val client = + OkHttpClient + .Builder() + .proxy(proxy) + .build() + + @Throws(IOException::class, ReCaptchaException::class) + override fun execute(request: Request): Response { + val httpMethod = request.httpMethod() + val url = request.url() + val headers = request.headers() + val dataToSend = request.dataToSend() + + val requestBuilder = + okhttp3.Request + .Builder() + .method(httpMethod, dataToSend?.toRequestBody()) + .url(url) + .addHeader("User-Agent", YouTubeClient.USER_AGENT_WEB) + + headers.forEach { (headerName, headerValueList) -> + if (headerValueList.size > 1) { + requestBuilder.removeHeader(headerName) + headerValueList.forEach { headerValue -> + requestBuilder.addHeader(headerName, headerValue) + } + } else if (headerValueList.size == 1) { + requestBuilder.header(headerName, headerValueList[0]) + } + } + + val response = client.newCall(requestBuilder.build()).execute() + + if (response.code == 429) { + response.close() + throw ReCaptchaException("reCaptcha Challenge requested", url) + } + + val responseBodyToReturn = response.body?.string() + + val latestUrl = response.request.url.toString() + return Response(response.code, response.message, response.headers.toMultimap(), responseBodyToReturn, latestUrl) + } +} \ No newline at end of file diff --git a/lyricsProviders/src/main/AndroidManifest.xml b/lyricsProviders/src/main/AndroidManifest.xml index a5918e68..f8085394 100644 --- a/lyricsProviders/src/main/AndroidManifest.xml +++ b/lyricsProviders/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 375b9e95..d61f1bd9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ pluginManagement { google() mavenCentral() gradlePluginPortal() + maven { setUrl("https://jitpack.io") } maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } @@ -23,4 +24,4 @@ rootProject.name = "SimpMusic" include("app") include(":kotlinYtmusicScraper") include(":spotify") -include(":lyricsProviders") +include(":lyricsProviders") \ No newline at end of file