From 9b7f0dcd047f31423a2c8c119395fcad491ae7d9 Mon Sep 17 00:00:00 2001 From: Malopieds Date: Mon, 28 Oct 2024 21:00:59 +0100 Subject: [PATCH] fix: play song without login reversed cna function for n and signature function, transpiled into kotlin --- .../zionhuang/music/playback/MusicService.kt | 5 +- .../java/com/zionhuang/innertube/InnerTube.kt | 14 ++++ .../java/com/zionhuang/innertube/YouTube.kt | 5 +- .../innertube/models/body/PlayerBody.kt | 14 +++- .../models/response/PlayerResponse.kt | 4 ++ .../com/zionhuang/innertube/utils/Utils.kt | 66 +++++++++++++++++++ 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt index 378f74a8a..96b0bfbe1 100644 --- a/app/src/main/java/com/zionhuang/music/playback/MusicService.kt +++ b/app/src/main/java/com/zionhuang/music/playback/MusicService.kt @@ -703,9 +703,8 @@ class MusicService : MediaLibraryService(), ) } scope.launch(Dispatchers.IO) { recoverSong(mediaId, playerResponse) } - - songUrlCache[mediaId] = format.url!! to playerResponse.streamingData!!.expiresInSeconds * 1000L - dataSpec.withUri(format.url!!.toUri()).subrange(dataSpec.uriPositionOffset, CHUNK_LENGTH) + songUrlCache[mediaId] = format.findUrl() to playerResponse.streamingData!!.expiresInSeconds * 1000L + dataSpec.withUri(format.findUrl().toUri()).subrange(dataSpec.uriPositionOffset, CHUNK_LENGTH) } } diff --git a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt index ae0775f1d..e43324f9a 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt @@ -5,8 +5,10 @@ import com.zionhuang.innertube.models.Context import com.zionhuang.innertube.models.YouTubeClient import com.zionhuang.innertube.models.YouTubeLocale import com.zionhuang.innertube.models.body.* +import com.zionhuang.innertube.utils.nSigDecode import com.zionhuang.innertube.utils.parseCookieString import com.zionhuang.innertube.utils.sha1 +import com.zionhuang.innertube.utils.sigDecode import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.* @@ -244,4 +246,16 @@ class InnerTube { ytClient(client, setLogin = true) setBody(AccountMenuBody(client.toContext(locale, visitorData))) } + + fun decodeCipher(cipher: String): String? { + val params = parseQueryString(cipher) + val signature = params["s"] ?: return null + val signatureParam = params["sp"] ?: return null + val url = params["url"]?.let { URLBuilder(it) } ?: return null + val n = url.parameters["n"] + url.parameters["n"] = nSigDecode(n.toString()) + url.parameters[signatureParam] = sigDecode(signature) + url.parameters["c"] = "ANDROID_MUSIC" + return url.toString() + } } diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index db91f83aa..28911d3e4 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -425,8 +425,9 @@ object YouTube { return@runCatching playerResponse } } - playerResponse = innerTube.player(IOS, videoId, playlistId).body() - if (playerResponse.playabilityStatus.status == "OK") { + playerResponse = innerTube.player(WEB_REMIX, videoId, playlistId).body() + if (playerResponse.playabilityStatus.status == "OK" && playerResponse.streamingData?.adaptiveFormats?.any + { it.url != null || it.signatureCipher != null } == true) { return@runCatching playerResponse } val safePlayerResponse = innerTube.player(TVHTML5, videoId, playlistId).body() diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/body/PlayerBody.kt b/innertube/src/main/java/com/zionhuang/innertube/models/body/PlayerBody.kt index 3522294db..c89cb0efb 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/body/PlayerBody.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/body/PlayerBody.kt @@ -9,4 +9,16 @@ data class PlayerBody( val videoId: String, val playlistId: String?, val contentCheckOk: Boolean = true, -) + val cpn: String? = "wzf9Y0nqz6AUe2Vr", // need some random cpn to get same algorithm for sig + val playbackContext: PlaybackContext? = PlaybackContext(ContentPlaybackContext(20019L)), +) { + @Serializable + data class PlaybackContext( + val contentPlaybackContext: ContentPlaybackContext?, + ) + + @Serializable + data class ContentPlaybackContext( + val signatureTimestamp: Long?, + ) +} \ No newline at end of file diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt index 23d426879..722a5e724 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/PlayerResponse.kt @@ -1,5 +1,6 @@ package com.zionhuang.innertube.models.response +import com.zionhuang.innertube.InnerTube import com.zionhuang.innertube.models.ResponseContext import com.zionhuang.innertube.models.Thumbnails import kotlinx.serialization.Serializable @@ -57,9 +58,12 @@ data class PlayerResponse( val audioChannels: Int?, val loudnessDb: Double?, val lastModified: Long?, + val signatureCipher: String?, ) { val isAudio: Boolean get() = width == null + + fun findUrl() = url ?: signatureCipher?.let { InnerTube().decodeCipher(it) }!! } } diff --git a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt index 2e727f0e6..07ac014e0 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt @@ -47,3 +47,69 @@ fun String.parseTime(): Int? { } return null } + +fun nSigDecode(n: String): String { + val step1 = + buildString { + append(n[8]) + append(n.substring(2, 8)) + append(n[1]) + append(n.substring(9)) + } + + val step2 = + buildString { + append(step1.substring(7)) + append((step1[0] + step1.substring(1, 3).reversed() + step1[3]).reversed()) + append(step1.substring(4, 7)) + } + + val step3 = step2.substring(7) + step2.substring(0, 7) + + val step4 = + buildString { + append(step3[step3.length - 4]) + append(step3.substring(3, 7)) + append(step3[2]) + append(step3.substring(8, 11)) + append(step3[7]) + append(step3.takeLast(3)) + append(step3[1]) + } + + val step5 = (step4.substring(0, 2) + step4.last() + step4.substring(3, step4.length - 1) + step4[2]).reversed() + + val keyString = "cbrrC5" + val charset = ('A'..'Z') + ('a'..'z') + ('0'..'9') + listOf('-', '_') + val mutableKeyList = keyString.toMutableList() + + val transformedChars = CharArray(step5.length) + + for (index in step5.indices) { + val currentChar = step5[index] + val indexInCharset = + (charset.indexOf(currentChar) - charset.indexOf(mutableKeyList[index % mutableKeyList.size]) + index + charset.size - index) % + charset.size + transformedChars[index] = charset[indexInCharset] + mutableKeyList[index % mutableKeyList.size] = transformedChars[index] + } + + val step6 = String(transformedChars) + return step6.dropLast(3).reversed() + step6.takeLast(3) +} + +fun sigDecode(input: String): String { + val middleSection = input.substring(3, input.length - 3) + val rearranged = (middleSection.take(35) + input[0] + middleSection.drop(36)).reversed() + val result = + buildString { + append("A") + append(rearranged.substring(0, 15)) + append(input[input.length - 2]) + append(rearranged.substring(16, 34)) + append(input[input.length - 3]) + append(rearranged.substring(35)) + append(input[38]) + } + return result +}