JIO-Saavn (WIP)

This commit is contained in:
shabinder 2021-05-23 00:07:16 +05:30
parent 28d3b53582
commit 67361b1337
5 changed files with 242 additions and 2 deletions

View File

@ -0,0 +1,51 @@
package jiosaavn
import analytics_html_img.client
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.get
import io.ktor.http.Parameters
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
interface JioSaavnRequests {
fun searchForSong(
queryURL: String
) {
}
suspend fun getSong(
ID: String,
fetchLyrics: Boolean = false
): JsonObject {
return ((Json.parseToJsonElement(client.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
.formatData(fetchLyrics)
}
suspend fun getSongID(
queryURL: String,
fetchLyrics: Boolean = false
): String? {
val res = client.get<String>(queryURL) {
body = FormDataContent(
Parameters.build {
append("bitrate", "320")
}
)
}
return try {
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
} catch (e: IndexOutOfBoundsException) {
res.split("\"pid\":\"").getOrNull(1)?.split("\",\"")?.firstOrNull()
}
}
companion object {
// EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="
const val song_details_base_url = "https://www.jiosaavn.com/api.php?__call=song.getDetails&cc=in&_marker=0%3F_marker%3D0&_format=json&pids="
const val album_details_base_url = "https://www.jiosaavn.com/api.php?__call=content.getAlbumDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&albumid="
const val playlist_details_base_url = "https://www.jiosaavn.com/api.php?__call=playlist.getDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&listid="
const val lyrics_base_url = "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&ctx=web6dot0&api_version=4&_format=json&_marker=0%3F_marker%3D0&lyrics_id="
}
}

View File

@ -0,0 +1,89 @@
package jiosaavn
import io.ktor.util.InternalAPI
import io.ktor.util.decodeBase64Bytes
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import utils.unescape
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.DESKeySpec
internal fun JsonObject.formatData(
includeLyrics: Boolean = false
): JsonObject {
return buildJsonObject {
// Accommodate Incoming Json Object Data
// And `Format` everything while iterating
this@formatData.forEach {
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
put(it.key, it.value.jsonPrimitive.content.format())
} else {
put(it.key, it.value)
}
}
try {
var url = getString("media_preview_url")!!.replace("preview", "aac") // We Will catch NPE
url = if (getBoolean("320kbps") == true) {
url.replace("_96_p.mp4", "_320.mp4")
} else {
url.replace("_96_p.mp4", "_160.mp4")
}
// Add Media URL to JSON Object
put("media_url", url)
} catch (e: Exception) {
e.printStackTrace()
// DECRYPT Encrypted Media URL
getString("encrypted_media_url")?.let {
put("media_url", decryptURL(it))
}
// Check if 320 Kbps is available or not
if (getBoolean("320kbps") != true && containsKey("media_url")) {
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
}
}
put("image", getString("image")?.replace("150x150", "500x500"))
}
}
@OptIn(InternalAPI::class)
fun decryptURL(url: String): String {
val dks = DESKeySpec("38346591".toByteArray())
val keyFactory = SecretKeyFactory.getInstance("DES")
val key: SecretKey = keyFactory.generateSecret(dks)
val cipher: Cipher = Cipher.getInstance("DES/ECB/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, key, SecureRandom())
}
return cipher.doFinal(url.decodeBase64Bytes())
.decodeToString()
.replace("_96.mp4", "_320.mp4")
}
internal fun String.format(): String {
return this.unescape()
.replace("&quot;", "'")
.replace("&amp;", "&")
.replace("&#039;", "'")
}
fun JsonObject.getString(key: String): String? = this[key]?.jsonPrimitive?.content
fun JsonObject.getLong(key: String): Long = this[key]?.jsonPrimitive?.content?.toLongOrNull() ?: 0
fun JsonObject.getInteger(key: String): Int = this[key]?.jsonPrimitive?.content?.toIntOrNull() ?: 0
fun JsonObject.getBoolean(key: String): Boolean? = this[key]?.jsonPrimitive?.content?.toBoolean()
fun JsonObject.getFloat(key: String): Float? = this[key]?.jsonPrimitive?.content?.toFloatOrNull()
fun JsonObject.getDouble(key: String): Double? = this[key]?.jsonPrimitive?.content?.toDoubleOrNull()
fun JsonObject?.getJsonObject(key: String): JsonObject? = this?.get(key)?.jsonObject
fun JsonArray?.getJsonObject(index: Int): JsonObject? = this?.get(index)?.jsonObject
fun JsonObject?.getJsonArray(key: String): JsonArray? = this?.get(key)?.jsonArray

View File

@ -0,0 +1,92 @@
package utils
/*
* JSON UTILS
* */
fun String.escape(): String {
val output = StringBuilder()
for (element in this) {
val chx = element.toInt()
assert(chx != 0)
when {
element == '\n' -> {
output.append("\\n")
}
element == '\t' -> {
output.append("\\t")
}
element == '\r' -> {
output.append("\\r")
}
element == '\\' -> {
output.append("\\\\")
}
element == '"' -> {
output.append("\\\"")
}
element == '\b' -> {
output.append("\\b")
}
chx >= 0x10000 -> {
assert(false) { "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't." }
}
chx > 127 -> {
output.append(String.format("\\u%04x", chx))
}
else -> {
output.append(element)
}
}
}
return output.toString()
}
fun String.unescape(): String {
val builder = StringBuilder()
var i = 0
while (i < this.length) {
val delimiter = this[i]
i++ // consume letter or backslash
if (delimiter == '\\' && i < this.length) {
// consume first after backslash
val ch = this[i]
i++
when (ch) {
'\\', '/', '"', '\'' -> {
builder.append(ch)
}
'n' -> builder.append('\n')
'r' -> builder.append('\r')
't' -> builder.append(
'\t'
)
'b' -> builder.append('\b')
'f' -> builder.append("\\f")
'u' -> {
val hex = StringBuilder()
// expect 4 digits
if (i + 4 > this.length) {
throw RuntimeException("Not enough unicode digits! ")
}
for (x in this.substring(i, i + 4).toCharArray()) {
if (!Character.isLetterOrDigit(x)) {
throw RuntimeException("Bad character in unicode escape.")
}
hex.append(Character.toLowerCase(x))
}
i += 4 // consume those four digits.
val code = hex.toString().toInt(16)
builder.append(code.toChar())
}
else -> {
throw RuntimeException("Illegal escape sequence: \\$ch")
}
}
} else { // it's not a backslash, or it's the last character.
builder.append(delimiter)
}
}
return builder.toString()
}

View File

@ -1,6 +1,14 @@
package utils
import jiosaavn.JioSaavnRequests
import kotlinx.coroutines.runBlocking
// Test Class- at development Time
fun main() = runBlocking {}
fun main() = runBlocking {
val jioSaavnClient = object : JioSaavnRequests {}
val resp = jioSaavnClient.getSongID(
queryURL = "https://www.jiosaavn.com/song/nadiyon-paar-let-the-music-play-again-from-roohi/KAM0bj1AAn4"
)
debug(jioSaavnClient.getSong(resp.toString()).toString())
}

View File

@ -60,7 +60,7 @@ dependencies {
}
kotlin {
js() {
js {
//useCommonJs()
browser {
webpackTask {