Skip to content

Commit

Permalink
use NewPipeExtractor to deobfuscate params + use WEB_REMIX client for…
Browse files Browse the repository at this point in the history
… logged in player requests + don't send login for IOS client
  • Loading branch information
gechoto authored Dec 24, 2024
1 parent bb11093 commit d4f9aad
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 22 deletions.
9 changes: 8 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,11 @@
# Keep Data data classes
-keep class com.my.kizzy.remote.** { <fields>; }
# Keep Gateway data classes
-keep class com.my.kizzy.gateway.entities.** { <fields>; }
-keep class com.my.kizzy.gateway.entities.** { <fields>; }

## Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.**
6 changes: 5 additions & 1 deletion app/src/main/java/com/zionhuang/music/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ class App : Application(), ImageLoaderFactory {
dataStore.data
.map { it[InnerTubeCookieKey] }
.distinctUntilChanged()
.collect { cookie ->
.collect { rawCookie ->
// quick hack until https://github.com/z-huang/InnerTune/pull/1694 is done
val isLoggedIn: Boolean = rawCookie?.contains("SAPISID") ?: false
val cookie = if (isLoggedIn) rawCookie else null

YouTube.cookie = cookie
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@ class DownloadUtil @Inject constructor(
)
}

songUrlCache[mediaId] = format.url!! to playerResponse.streamingData!!.expiresInSeconds * 1000L
dataSpec.withUri(format.url!!.toUri())
val streamUrl = playerResponse.findUrl(format.itag)

songUrlCache[mediaId] = streamUrl!! to playerResponse.streamingData!!.expiresInSeconds * 1000L
dataSpec.withUri(streamUrl.toUri())
}
val downloadNotificationHelper = DownloadNotificationHelper(context, ExoDownloadService.CHANNEL_ID)
val downloadManager: DownloadManager = DownloadManager(context, databaseProvider, downloadCache, dataSourceFactory, Executor(Runnable::run)).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -691,8 +691,10 @@ 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)
val streamUrl = playerResponse.findUrl(format.itag)

songUrlCache[mediaId] = streamUrl!! to playerResponse.streamingData!!.expiresInSeconds * 1000L
dataSpec.withUri(streamUrl.toUri()).subrange(dataSpec.uriPositionOffset, CHUNK_LENGTH)
}
}

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ firebase-perf-plugin = { module = "com.google.firebase:perf-plugin", version = "
mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version = "17.0.6" }
mlkit-translate = { group = "com.google.mlkit", name = "translate", version = "17.0.3" }

newpipe-extractor = { group = "com.github.TeamNewPipe", name = "NewPipeExtractor", version = "v0.24.3" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Expand Down
1 change: 1 addition & 0 deletions innertube/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ dependencies {
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.encoding)
implementation(libs.brotli)
implementation(libs.newpipe.extractor)
testImplementation(libs.junit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class InnerTube {
if (client.referer != null) {
append("Referer", client.referer)
}
if (setLogin) {
if (setLogin && client.supportsLogin) {
cookie?.let { cookie ->
append("cookie", cookie)
if ("SAPISID" !in cookieMap) return@let
Expand Down
11 changes: 8 additions & 3 deletions innertube/src/main/java/com/zionhuang/innertube/YouTube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import org.schabi.newpipe.extractor.NewPipe
import java.net.Proxy

/**
Expand All @@ -63,6 +64,10 @@ import java.net.Proxy
object YouTube {
private val innerTube = InnerTube()

init {
NewPipe.init(NewPipeDownloaderImpl)
}

var locale: YouTubeLocale
get() = innerTube.locale
set(value) {
Expand Down Expand Up @@ -431,8 +436,8 @@ object YouTube {

suspend fun player(videoId: String, playlistId: String? = null): Result<PlayerResponse> = runCatching {
var playerResponse: PlayerResponse
if (this.cookie != null) { // if logged in: try ANDROID_MUSIC client first because IOS client does not play age restricted songs
playerResponse = innerTube.player(ANDROID_MUSIC, videoId, playlistId).body<PlayerResponse>()
if (this.cookie != null) { // if logged in: try WEB_REMIX client first because IOS client does not support login
playerResponse = innerTube.player(WEB_REMIX, videoId, playlistId).body<PlayerResponse>()
if (playerResponse.playabilityStatus.status == "OK") {
return@runCatching playerResponse
}
Expand All @@ -443,7 +448,7 @@ object YouTube {
}
val safePlayerResponse = innerTube.player(TVHTML5, videoId, playlistId).body<PlayerResponse>()
if (safePlayerResponse.playabilityStatus.status != "OK") {
return@runCatching playerResponse
return@runCatching safePlayerResponse
}
val audioStreams = innerTube.pipedStreams(videoId).body<PipedResponse>().audioStreams
safePlayerResponse.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class YouTubeClient(
val userAgent: String,
val osVersion: String? = null,
val referer: String? = null,
val supportsLogin: Boolean = false,
) {
fun toContext(locale: YouTubeLocale, visitorData: String?) = Context(
client = Context.Client(
Expand All @@ -25,15 +26,14 @@ data class YouTubeClient(
companion object {
private const val REFERER_YOUTUBE_MUSIC = "https://music.youtube.com/"

private const val USER_AGENT_WEB = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36"
private const val USER_AGENT_WEB = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
private const val USER_AGENT_ANDROID = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Mobile Safari/537.36"
private const val USER_AGENT_IOS = "com.google.ios.youtube/19.29.1 (iPhone16,2; U; CPU iOS 17_5_1 like Mac OS X;)"

val ANDROID_MUSIC = YouTubeClient(
clientName = "ANDROID_MUSIC",
clientVersion = "5.01",
api_key = "AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI",
userAgent = USER_AGENT_ANDROID
userAgent = USER_AGENT_ANDROID,
)

val ANDROID = YouTubeClient(
Expand All @@ -47,30 +47,31 @@ data class YouTubeClient(
clientName = "WEB",
clientVersion = "2.2021111",
api_key = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX3",
userAgent = USER_AGENT_WEB
userAgent = USER_AGENT_WEB,
)

val WEB_REMIX = YouTubeClient(
clientName = "WEB_REMIX",
clientVersion = "1.20220606.03.00",
api_key = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30",
clientVersion = "1.20241127.01.00",
api_key = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", // TODO: remove
userAgent = USER_AGENT_WEB,
referer = REFERER_YOUTUBE_MUSIC
referer = REFERER_YOUTUBE_MUSIC,
supportsLogin = true,
)

val TVHTML5 = YouTubeClient(
clientName = "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
clientVersion = "2.0",
api_key = "AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8",
userAgent = "Mozilla/5.0 (PlayStation 4 5.55) AppleWebKit/601.2 (KHTML, like Gecko)"
userAgent = "Mozilla/5.0 (PlayStation 4 5.55) AppleWebKit/601.2 (KHTML, like Gecko)",
)

val IOS = YouTubeClient(
clientName = "IOS",
clientVersion = "19.29.1",
api_key = "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc",
userAgent = USER_AGENT_IOS,
osVersion = "17.5.1.21F90",
clientVersion = "19.45.4",
api_key = "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc", // TODO: remove
userAgent = "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)",
osVersion = "18.1.0.22B83",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package com.zionhuang.innertube.models.response

import com.zionhuang.innertube.models.ResponseContext
import com.zionhuang.innertube.models.Thumbnails
import io.ktor.http.URLBuilder
import io.ktor.http.parseQueryString
import kotlinx.serialization.Serializable
import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptPlayerManager

/**
* PlayerResponse with [com.zionhuang.innertube.models.YouTubeClient.ANDROID_MUSIC] client
Expand Down Expand Up @@ -57,6 +60,7 @@ data class PlayerResponse(
val audioChannels: Int?,
val loudnessDb: Double?,
val lastModified: Long?,
val signatureCipher: String?,
) {
val isAudio: Boolean
get() = width == null
Expand All @@ -74,4 +78,22 @@ data class PlayerResponse(
val viewCount: String,
val thumbnail: Thumbnails,
)

fun findUrl(itag: Int): String? {
this.streamingData?.adaptiveFormats?.find { it.itag == itag }?.let { format ->
if (format.url != null) {
return format.url
}
if (this.videoDetails?.videoId != null && format.signatureCipher != null) {
val params = parseQueryString(format.signatureCipher)
val obfuscatedSignature = params["s"] ?: return null
val signatureParam = params["sp"] ?: return null
val url = params["url"]?.let { URLBuilder(it) } ?: return null
url.parameters[signatureParam] = YoutubeJavaScriptPlayerManager.deobfuscateSignature("", obfuscatedSignature)
val streamUrl = YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated("", url.toString())
return streamUrl
}
}
return null
}
}

0 comments on commit d4f9aad

Please sign in to comment.