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..b2613a4b1 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt @@ -4,22 +4,36 @@ import com.zionhuang.innertube.encoder.brotli 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.models.body.AccountMenuBody +import com.zionhuang.innertube.models.body.BrowseBody +import com.zionhuang.innertube.models.body.GetQueueBody +import com.zionhuang.innertube.models.body.GetSearchSuggestionsBody +import com.zionhuang.innertube.models.body.GetTranscriptBody +import com.zionhuang.innertube.models.body.NextBody +import com.zionhuang.innertube.models.body.PlayerBody +import com.zionhuang.innertube.models.body.SearchBody import com.zionhuang.innertube.utils.parseCookieString import com.zionhuang.innertube.utils.sha1 -import io.ktor.client.* -import io.ktor.client.engine.okhttp.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.compression.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultRequest +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.http.userAgent +import io.ktor.serialization.kotlinx.json.json import io.ktor.util.encodeBase64 import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import java.net.Proxy -import java.util.* +import java.util.Locale /** * Provide access to InnerTube endpoints. 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..b749a7107 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 @@ -2,6 +2,7 @@ package com.zionhuang.innertube.models.response import com.zionhuang.innertube.models.ResponseContext import com.zionhuang.innertube.models.Thumbnails +import com.zionhuang.innertube.utils.decodeCipher 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 { 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..d2d63a435 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt @@ -2,6 +2,8 @@ package com.zionhuang.innertube.utils import com.zionhuang.innertube.YouTube import com.zionhuang.innertube.pages.PlaylistPage +import io.ktor.http.URLBuilder +import io.ktor.http.parseQueryString import java.security.MessageDigest suspend fun Result.completed() = runCatching { @@ -47,3 +49,81 @@ 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 +} + +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() +}