diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt index 969b1a2..e74255c 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt @@ -136,9 +136,9 @@ class DeezerApi(private val session: DeezerSession) { } } - private val client: OkHttpClient get() = createOkHttpClient(useProxy = true) - private val clientLog: OkHttpClient get() = createOkHttpClient(useProxy = true , true) - private val clientNP: OkHttpClient get() = createOkHttpClient(useProxy = false) + private val client: OkHttpClient by lazy { createOkHttpClient(useProxy = true) } + private val clientLog: OkHttpClient by lazy { createOkHttpClient(useProxy = true , true) } + private val clientNP: OkHttpClient by lazy { createOkHttpClient(useProxy = false) } private fun getHeaders(method: String? = ""): Headers { return Headers.Builder().apply { @@ -161,7 +161,11 @@ class DeezerApi(private val session: DeezerSession) { }.build() } - suspend fun callApi(method: String, params: JsonObject = buildJsonObject { }, gatewayInput: String? = ""): String = withContext(Dispatchers.IO) { + suspend fun callApi( + method: String, + params: JsonObject = buildJsonObject { }, + gatewayInput: String? = "" + ): String = withContext(Dispatchers.IO) { val url = HttpUrl.Builder() .scheme("https") .host("www.deezer.com") @@ -191,34 +195,32 @@ class DeezerApi(private val session: DeezerSession) { } .build() - val response = client.newCall(request).await() - val responseBody = response.body.string() + client.newCall(request).await().use { response -> + val responseBody = response.body.string() + if (!response.isSuccessful) throw Exception("API call failed with status ${response.code}: $responseBody") - if (!response.isSuccessful) { - throw Exception("API call failed with status code ${response.code}: $responseBody") - } - - if (method == "deezer.getUserData") { - response.headers.forEach { - if (it.second.startsWith("sid=")) { - session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) + if (method == "deezer.getUserData") { + response.headers.forEach { + if (it.second.startsWith("sid=")) { + session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) + } } } - } - if (responseBody.contains("\"VALID_TOKEN_REQUIRED\":\"Invalid CSRF token\"")) { - if (email.isEmpty() && pass.isEmpty()) { - session.isArlExpired(true) - throw Exception("Please re-login (Best use User + Pass method)") - } else { - session.isArlExpired(false) - val userList = DeezerExtension().onLogin(email, pass) - DeezerExtension().onSetLoginUser(userList.first()) - return@withContext callApi(method, params, gatewayInput) + if (responseBody.contains("\"VALID_TOKEN_REQUIRED\":\"Invalid CSRF token\"")) { + if (email.isEmpty() && pass.isEmpty()) { + session.isArlExpired(true) + throw Exception("Please re-login (Best use User + Pass method)") + } else { + session.isArlExpired(false) + val userList = DeezerExtension().onLogin(email, pass) + DeezerExtension().onSetLoginUser(userList.first()) + return@withContext callApi(method, params, gatewayInput) + } } - } - responseBody + responseBody + } } //<============= Login =============> @@ -253,29 +255,33 @@ class DeezerApi(private val session: DeezerSession) { } suspend fun getArlByEmail(mail: String, password: String) { - // Get SID - getSid() - - val clientId = "447462" - val clientSecret = "a83bf7f38ad2f137e444727cfc3775cf" - val md5Password = md5(password) - - val params = mapOf( - "app_id" to clientId, - "login" to mail, - "password" to md5Password, - "hash" to md5(clientId + mail + md5Password + clientSecret) - ) - - // Get access token - val responseJson = getToken(params, sid) - val apiResponse = json.decodeFromString(responseJson) - session.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) + try { + // Get SID + getSid() + + val clientId = "447462" + val clientSecret = "a83bf7f38ad2f137e444727cfc3775cf" + val md5Password = md5(password) + + val params = mapOf( + "app_id" to clientId, + "login" to mail, + "password" to md5Password, + "hash" to md5(clientId + mail + md5Password + clientSecret) + ) - // Get ARL - val arlResponse = callApi("user.getArl") - val arlObject = json.decodeFromString(arlResponse) - session.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) + // Get access token + val responseJson = getToken(params, sid) + val apiResponse = json.decodeFromString(responseJson) + session.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) + + // Get ARL + val arlResponse = callApi("user.getArl") + val arlObject = json.decodeFromString(arlResponse) + session.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) + } catch (e: Exception) { + getArlByEmail(mail, password) + } } private fun md5(input: String): String { diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/LocalAudioWebServer.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/LocalAudioWebServer.kt index e728135..86720c1 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/LocalAudioWebServer.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/LocalAudioWebServer.kt @@ -167,13 +167,24 @@ object AudioStreamManager { private var server: LocalAudioWebServer? = null private val lock = Any() private const val HOSTNAME = "127.0.0.1" - private const val PORT = 36958 + + @Volatile + private var usedPort = -1 private fun startServerIfNeeded() { synchronized(lock) { if (server == null) { - server = LocalAudioWebServer(HOSTNAME, PORT).apply { - start(SOCKET_READ_TIMEOUT, false) + try { + server = LocalAudioWebServer(HOSTNAME, 0).apply { + start(SOCKET_READ_TIMEOUT, false) + } + usedPort = server?.listeningPort ?: -1 + + println("LocalAudioWebServer started on port: $usedPort") + } catch (e: Exception) { + println("Failed to start LocalAudioWebServer: ${e.message}") + e.printStackTrace() + server = null } } } @@ -195,7 +206,7 @@ object AudioStreamManager { } fun getStreamUrlForTrack(trackId: String): String { - return "http://$HOSTNAME:$PORT/stream?trackId=$trackId" + return "http://$HOSTNAME:$usedPort/stream?trackId=$trackId" } } diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt index db7bcad..30c121a 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt @@ -21,39 +21,32 @@ import javax.crypto.spec.SecretKeySpec object Utils { private const val SECRET = "g4el58wc0zvf9na1" - private val secretIvSpec = IvParameterSpec(byteArrayOf(0,1,2,3,4,5,6,7)) - + private val secretIvSpec = IvParameterSpec(ByteArray(8) { it.toByte() }) private val keySpecCache = ConcurrentHashMap() - private fun bitwiseXor(firstVal: Char, secondVal: Char, thirdVal: Char): Char { - return (firstVal.code xor secondVal.code xor thirdVal.code).toChar() + private val md5Digest: MessageDigest by lazy { MessageDigest.getInstance("MD5") } + + private fun bitwiseXor(vararg values: Char): Char { + return values.fold(0) { acc, char -> acc xor char.code }.toChar() } fun createBlowfishKey(trackId: String): String { val trackMd5Hex = trackId.toMD5() - val blowfishKey = StringBuilder() - - for (i in 0..15) { - val nextChar = bitwiseXor(trackMd5Hex[i], trackMd5Hex[i + 16], SECRET[i]) - blowfishKey.append(nextChar) + return buildString { + for (i in 0 until 16) { + append(bitwiseXor(trackMd5Hex[i], trackMd5Hex[i + 16], SECRET[i])) + } } - - return blowfishKey.toString() } private fun getSecretKeySpec(blowfishKey: String): SecretKeySpec { return keySpecCache.computeIfAbsent(blowfishKey) { - SecretKeySpec(blowfishKey.toByteArray(), "Blowfish") + SecretKeySpec(blowfishKey.toByteArray(Charsets.ISO_8859_1), "Blowfish") } } - private fun bytesToHex(bytes: ByteArray): String { - return bytes.joinToString("") { String.format("%02X", it) } - } - private fun String.toMD5(): String { - val bytes = MessageDigest.getInstance("MD5").digest(this.toByteArray(Charsets.ISO_8859_1)) - return bytesToHex(bytes).lowercase() + return md5Digest.digest(toByteArray(Charsets.ISO_8859_1)).joinToString("") { "%02x".format(it) } } fun decryptBlowfish(chunk: ByteArray, blowfishKey: String): ByteArray { diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt index 0bf9bbb..e39bb64 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt @@ -97,14 +97,6 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea return browseSections.mapNotNull { section -> val id = section.jsonObject["module_id"]!!.jsonPrimitive.content when (id) { - "67aa1c1b-7873-488d-88a0-55b6596cf4d6", "486313b7-e3c7-453d-ba79-27dc6bea20ce", - "1d8dfed4-582f-40e1-b29c-760b44c0301e", "ecb89e7c-1c07-4922-aa50-d29745576636", - "64ac680b-7c84-49a3-9077-38e9b653332e" -> { - parser.run { - section.toShelfItemsList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) - } - } - "8b2c6465-874d-4752-a978-1637ca0227b5" -> { parser.run { section.toShelfCategoryList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) { target -> @@ -113,6 +105,12 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea } } + !in "6550abfd-15e4-47de-a5e8-a60e27fa152a", !in "c8b406d4-5293-4f59-a0f4-562eba496a0b" -> { + parser.run { + section.toShelfItemsList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) + } + } + else -> null } }