Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: play song without login #1690

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
34 changes: 24 additions & 10 deletions innertube/src/main/java/com/zionhuang/innertube/InnerTube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,9 @@ object YouTube {
return@runCatching playerResponse
}
}
playerResponse = innerTube.player(IOS, videoId, playlistId).body<PlayerResponse>()
if (playerResponse.playabilityStatus.status == "OK") {
playerResponse = innerTube.player(WEB_REMIX, videoId, playlistId).body<PlayerResponse>()
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<PlayerResponse>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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) }!!
}
}

Expand Down
80 changes: 80 additions & 0 deletions innertube/src/main/java/com/zionhuang/innertube/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<PlaylistPage>.completed() = runCatching {
Expand Down Expand Up @@ -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()
}