Skip to content

Commit

Permalink
Probably major improvement to loading the songs & use okhttp:5.0.0-al…
Browse files Browse the repository at this point in the history
…pha.14
  • Loading branch information
LuftVerbot committed Jul 28, 2024
1 parent 1cf0702 commit 4971f6a
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 88 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ dependencies {
val libVersion = "38e1df03f6"
compileOnly("com.github.brahmkshatriya:echo:$libVersion")

implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
Expand Down
16 changes: 8 additions & 8 deletions app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ class DeezerApi(private val settings: Settings = Settings()) {
addInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
if (originalResponse.header("Content-Encoding") == "gzip") {
val gzipSource = GZIPInputStream(originalResponse.body?.byteStream())
val decompressedBody = gzipSource.readBytes().toResponseBody(originalResponse.body?.contentType())
val gzipSource = GZIPInputStream(originalResponse.body.byteStream())
val decompressedBody = gzipSource.readBytes().toResponseBody(originalResponse.body.contentType())
originalResponse.newBuilder().body(decompressedBody).build()
} else {
originalResponse
Expand All @@ -95,8 +95,8 @@ class DeezerApi(private val settings: Settings = Settings()) {
addInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
if (originalResponse.header("Content-Encoding") == "gzip") {
val gzipSource = GZIPInputStream(originalResponse.body?.byteStream())
val decompressedBody = gzipSource.readBytes().toResponseBody(originalResponse.body?.contentType())
val gzipSource = GZIPInputStream(originalResponse.body.byteStream())
val decompressedBody = gzipSource.readBytes().toResponseBody(originalResponse.body.contentType())
originalResponse.newBuilder().body(decompressedBody).build()
} else {
originalResponse
Expand Down Expand Up @@ -159,7 +159,7 @@ class DeezerApi(private val settings: Settings = Settings()) {
.build()

val response = client.newCall(request).execute()
val responseBody = response.body?.string().orEmpty()
val responseBody = response.body.string()

if (method == "deezer.getUserData") {
response.headers.forEach {
Expand Down Expand Up @@ -264,7 +264,7 @@ class DeezerApi(private val settings: Settings = Settings()) {

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Unexpected code $response")
return response.body?.string() ?: throw Exception("Empty response body")
return response.body.string()
}
}

Expand Down Expand Up @@ -327,7 +327,7 @@ class DeezerApi(private val settings: Settings = Settings()) {
.build()

val response = clientNP.newCall(request).execute()
val responseBody = response.body?.string().orEmpty()
val responseBody = response.body.string()

json.decodeFromString<JsonObject>(responseBody)
}
Expand Down Expand Up @@ -362,7 +362,7 @@ class DeezerApi(private val settings: Settings = Settings()) {
.build()

val response = clientNP.newCall(request).execute()
val responseBody = response.body?.string().orEmpty()
val responseBody = response.body.string()

json.decodeFromString<JsonObject>(responseBody)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import dev.brahmkshatriya.echo.common.models.User
import dev.brahmkshatriya.echo.common.settings.Setting
import dev.brahmkshatriya.echo.common.settings.SettingSwitch
import dev.brahmkshatriya.echo.common.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
Expand Down Expand Up @@ -370,10 +373,10 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, SearchClie
return if (streamable.quality == 1) {
StreamableAudio.StreamableRequest(streamable.id.toRequest())
} else {
getByteStreamAudio(streamable, client)
}

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

getByteStreamAudio(scope, streamable, client)
}
}

override suspend fun getStreamableVideo(streamable: Streamable) = throw Exception("not Used")
Expand Down
159 changes: 83 additions & 76 deletions app/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package dev.brahmkshatriya.echo.extension

import dev.brahmkshatriya.echo.common.models.Streamable
import dev.brahmkshatriya.echo.common.models.StreamableAudio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.ConnectionPool
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.internal.http2.StreamResetException
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.math.BigInteger
import java.security.MessageDigest
import java.util.Arrays
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
Expand Down Expand Up @@ -44,12 +51,12 @@ object Utils {
val secretKeySpec = SecretKeySpec(blowfishKey.toByteArray(), "Blowfish")
val thisTrackCipher = Cipher.getInstance("BLOWFISH/CBC/NoPadding")
thisTrackCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, secretIvSpec)
return thisTrackCipher.update(chunk)
return thisTrackCipher.doFinal(chunk)
}

fun getContentLength(url: String, client: OkHttpClient): Long {
var totalLength = 0L
val request = okhttp3.Request.Builder().url(url).head().build()
val request = Request.Builder().url(url).head().build()
val response = client.newCall(request).execute()
totalLength += response.header("Content-Length")?.toLong() ?: 0L
response.close()
Expand All @@ -70,99 +77,99 @@ fun String.toMD5(): String {
return bytesToHex(bytes).lowercase()
}

fun getByteStreamAudio(streamable: Streamable, client: OkHttpClient): StreamableAudio {
fun getByteStreamAudio(scope: CoroutineScope, streamable: Streamable, client: OkHttpClient): StreamableAudio {
val url = streamable.id
val contentLength = Utils.getContentLength(url, client)
val key = streamable.extra["key"]!!

val request = Request.Builder().url(url).build()
var decChunk = ByteArray(0)

runBlocking {
withContext(Dispatchers.IO) {
val response = client.newCall(request).execute()
val byteStream = response.body?.byteStream()
?: throw IOException("Failed to get byte stream from response")
val pipedInputStream = PipedInputStream()
val pipedOutputStream = PipedOutputStream(pipedInputStream)

val clientWithTimeouts = client.newBuilder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
.protocols(listOf(Protocol.HTTP_1_1))
.build()

scope.launch(Dispatchers.IO) {
retry(3) {
val response = clientWithTimeouts.newCall(request).execute()
val byteStream = response.body.byteStream().buffered()

try {
// Read the entire byte stream into memory
val completeStream = ByteArrayOutputStream()
val buffer = ByteArray(2 * 1024 * 1024) // Increased buffer size
var bytesRead: Int
while (byteStream.read(buffer).also { bytesRead = it } != -1) {
completeStream.write(buffer, 0, bytesRead)
}

// Ensure complete stream is read
val completeStreamBytes = completeStream.toByteArray()
println("Total bytes read: ${completeStreamBytes.size}")

// Determine chunk size based on decryption block size
val chunkSize = 2048 * 3072
val numChunks = (completeStreamBytes.size + chunkSize - 1) / chunkSize
println("Number of chunks: $numChunks")

// Measure decryption time
val startTime = System.nanoTime()

// Decrypt the chunks concurrently
val deferredChunks = (0 until numChunks).map { i ->
val start = i * chunkSize
val end = minOf((i + 1) * chunkSize, completeStreamBytes.size)
println("Chunk $i: start $start, end $end")
async(Dispatchers.Default) { decryptStreamChunk(completeStreamBytes.copyOfRange(start, end), key) }
}

// Wait for all decryption tasks to complete and concatenate the results
deferredChunks.forEach { deferred ->
decChunk += deferred.await()
val buffer = ByteArray(2048)
var totalRead: Int
var counter = 0

while (true) {
totalRead = 0
while (totalRead < 2048) {
val bytesRead = byteStream.read(buffer, totalRead, 2048 - totalRead)
if (bytesRead == -1) break
totalRead += bytesRead
}

if (totalRead == 0) break

if (totalRead == 2048) {
if (counter % 3 == 0) {
val decryptedChunk = withContext(Dispatchers.Default) {
Utils.decryptBlowfish(buffer, key)
}
pipedOutputStream.write(decryptedChunk)
} else {
pipedOutputStream.write(buffer, 0, 2048)
}
} else {
if (counter % 3 == 0) {
val partialBuffer = buffer.copyOf(totalRead)
val decryptedChunk = withContext(Dispatchers.Default) {
Utils.decryptBlowfish(partialBuffer, key)
}
pipedOutputStream.write(decryptedChunk, 0, totalRead)
} else {
pipedOutputStream.write(buffer, 0, totalRead)
}
}

counter++
pipedOutputStream.flush()
}

val endTime = System.nanoTime()
val duration = endTime - startTime
println("Decryption took ${duration / 1_000_000} milliseconds")
} catch (e: Exception) {
e.printStackTrace()
} finally {
response.close()
byteStream.close()
try {
response.close()
byteStream.close()
pipedOutputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}

return StreamableAudio.ByteStreamAudio(
stream = decChunk.inputStream(),
stream = pipedInputStream,
totalBytes = contentLength
)
}

private fun decryptStreamChunk(chunk: ByteArray, key: String): ByteArray {
val decryptedStream = ByteArrayOutputStream()
var place = 0

while (place < chunk.size) {
val remainingBytes = chunk.size - place
val blockSize = 2048
val encryptedBlock = blockSize * 3 // Every third block is encrypted

val currentChunkSize = min(remainingBytes, encryptedBlock)
val currentChunk = chunk.copyOfRange(place, place + currentChunkSize)
place += currentChunkSize

for (i in 0 until currentChunk.size step blockSize) {
val blockEnd = min(currentChunk.size, i + blockSize)
val block = currentChunk.copyOfRange(i, blockEnd)

if ((i / blockSize) % 3 == 0 && block.size == blockSize) {
val decryptedBlock = Utils.decryptBlowfish(block, key)
decryptedStream.write(decryptedBlock)
} else {
decryptedStream.write(block)
suspend fun retry(times: Int, block: suspend () -> Unit) {
repeat(times) {
try {
block()
return
} catch (e: StreamResetException) {
if (it == times - 1) {
throw e
}
delay(1000)
}
}

val decryptedBytes = decryptedStream.toByteArray()
println("Decrypted chunk size: ${decryptedBytes.size}")
return decryptedBytes
}

fun generateTrackUrl(trackId: String, md5Origin: String, mediaVersion: String, quality: Int): String {
Expand Down

0 comments on commit 4971f6a

Please sign in to comment.