diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt b/app/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt index e2ffe61..a400b1f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt @@ -7,6 +7,8 @@ import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder import dev.brahmkshatriya.echo.common.models.MediaItemsContainer import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonArray @@ -22,15 +24,28 @@ fun JsonObject.toMediaItemsContainer( return MediaItemsContainer.Category( title = name ?: "Unknown", list = itemsArray.mapNotNull { item -> - item.jsonObject.toEchoMediaPlaylistItem(api) + item.jsonObject.toEchoMediaItem(api) } ) } -fun JsonObject.toEchoMediaPlaylistItem( +fun JsonArray.toMediaItemsContainer( + api: DeezerApi = DeezerApi(), + name: String? +): MediaItemsContainer { + val itemsArray = jsonArray + return MediaItemsContainer.Category( + title = name ?: "Unknown", + list = itemsArray.mapNotNull { item -> + item.jsonObject.toEchoMediaItem(api) + } + ) +} + +fun JsonElement.toEchoMediaItem( api: DeezerApi ): EchoMediaItem? { - val data = jsonObject["data"]!!.jsonObject + val data = jsonObject["data"]?.jsonObject ?: jsonObject val type = data["__TYPE__"]!!.jsonPrimitive.content return when { type.contains("playlist") -> EchoMediaItem.Lists.PlaylistItem(toPlaylist(api)) @@ -42,8 +57,8 @@ fun JsonObject.toEchoMediaPlaylistItem( -fun JsonObject.toAlbum(): Album { - val data = jsonObject["data"]?.jsonObject ?: jsonObject["DATA"]!!.jsonObject +fun JsonElement.toAlbum(): Album { + val data = jsonObject["data"]?.jsonObject ?: jsonObject["DATA"]?.jsonObject ?: jsonObject return Album( id = data["ALB_ID"]?.jsonPrimitive?.content ?: "", title = data["ALB_TITLE"]?.jsonPrimitive?.content ?: "", @@ -53,7 +68,7 @@ fun JsonObject.toAlbum(): Album { ) } -fun JsonObject.toTrack(): Track { +fun JsonElement.toTrack(): Track { val data = jsonObject["data"]?.jsonObject ?: jsonObject return Track( id = data["SNG_ID"]!!.jsonPrimitive.content, @@ -66,12 +81,13 @@ fun JsonObject.toTrack(): Track { ) } -fun JsonObject.toPlaylist(api: DeezerApi): Playlist { - val data = jsonObject["data"]?.jsonObject ?: jsonObject["DATA"]!!.jsonObject +fun JsonElement.toPlaylist(api: DeezerApi): Playlist { + val data = jsonObject["data"]?.jsonObject ?: jsonObject["DATA"]?.jsonObject ?: jsonObject + val type = jsonObject["PICTURE_TYPE"]?.jsonPrimitive?.content return Playlist( id = data["PLAYLIST_ID"]?.jsonPrimitive?.content ?: "", title = data["TITLE"]?.jsonPrimitive?.content ?: "", - cover = getCover(jsonObject), + cover = getCover(jsonObject, type), description = data["DESCRIPTION"]?.jsonPrimitive?.content ?: "", subtitle = jsonObject["subtitle"]?.jsonPrimitive?.content ?: "", isEditable = data["PARENT_USER_ID"]!!.jsonPrimitive.content == api.userId, @@ -79,24 +95,30 @@ fun JsonObject.toPlaylist(api: DeezerApi): Playlist { ) } -fun getCover(jsonObject: JsonObject): ImageHolder { - if(jsonObject["pictures"]?.jsonArray != null) { - val pictureArray = jsonObject["pictures"]!!.jsonArray - val picObject = pictureArray.first().jsonObject - val md5 = picObject["md5"]!!.jsonPrimitive.content - val type = picObject["type"]!!.jsonPrimitive.content - val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/264x264-000000-80-0-0.jpg" - return url.toImageHolder() - } else if(jsonObject["DATA"]?.jsonObject != null) { - val dataObject = jsonObject["DATA"]!!.jsonObject - val md5 = dataObject["PLAYLIST_PICTURE"]?.jsonPrimitive?.content - ?: dataObject["ALB_PICTURE"]?.jsonPrimitive?.content ?: "" - val type = dataObject["PICTURE_TYPE"]?.jsonPrimitive?.content ?: "cover" +fun getCover(jsonObject: JsonObject, type: String? = null): ImageHolder { + if(type != null) { + val md5 = jsonObject["PLAYLIST_PICTURE"]!!.jsonPrimitive.content val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/264x264-000000-80-0-0.jpg" return url.toImageHolder() } else { - val md5 = jsonObject["ALB_PICTURE"]?.jsonPrimitive?.content ?: "" - val url = "https://e-cdns-images.dzcdn.net/images/cover/$md5/264x264-000000-80-0-0.jpg" - return url.toImageHolder() + if (jsonObject["pictures"]?.jsonArray != null) { + val pictureArray = jsonObject["pictures"]!!.jsonArray + val picObject = pictureArray.first().jsonObject + val md5 = picObject["md5"]!!.jsonPrimitive.content + val type = picObject["type"]!!.jsonPrimitive.content + val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/264x264-000000-80-0-0.jpg" + return url.toImageHolder() + } else if (jsonObject["DATA"]?.jsonObject != null) { + val dataObject = jsonObject["DATA"]!!.jsonObject + val md5 = dataObject["PLAYLIST_PICTURE"]?.jsonPrimitive?.content + ?: dataObject["ALB_PICTURE"]?.jsonPrimitive?.content ?: "" + val type = dataObject["PICTURE_TYPE"]?.jsonPrimitive?.content ?: "cover" + val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/264x264-000000-80-0-0.jpg" + return url.toImageHolder() + } else { + val md5 = jsonObject["ALB_PICTURE"]?.jsonPrimitive?.content ?: "" + val url = "https://e-cdns-images.dzcdn.net/images/cover/$md5/264x264-000000-80-0-0.jpg" + return url.toImageHolder() + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt b/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt index b7eecf7..792c1a5 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt @@ -1,6 +1,7 @@ package dev.brahmkshatriya.echo.extension import dev.brahmkshatriya.echo.common.models.Album +import dev.brahmkshatriya.echo.common.models.Artist import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track @@ -28,11 +29,11 @@ class Settings { } class DeezerApi( - private val arl: String = "", - private val sid: String = "", - private val token: String = "", - val userId: String = "", - private val licenseToken: String = "", + private var arl: String = "", + private var sid: String = "", + private var token: String = "", + var userId: String = "", + private var licenseToken: String = "", private var userName: String? = null, private var favoritesPlaylistId: String = "" ) { @@ -109,15 +110,6 @@ class DeezerApi( val responseBody = response.body?.string() val body = responseBody.toString() - // Grab SID - /*if (method == "deezer.getUserData") { - response.headers("Set-Cookie").forEach { cookie -> - if (cookie.startsWith("sid=")) { - sid = cookie.split("=")[1] - } - } - }*/ - body } @@ -174,7 +166,6 @@ class DeezerApi( headersBuilder.add("Connection", "Keep-alive") headersBuilder.add("Content-Type", "application/json; charset=utf-8") headersBuilder.add("Cookie", "arl=$arl&sid=$sid") - //headersBuilder.add("Host", "media.deezer.com") headersBuilder.add("Host", "media.deezer.com") headersBuilder.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36") val headers = headersBuilder.build() @@ -235,6 +226,30 @@ class DeezerApi( jObject } + suspend fun search(query: String): JsonObject { + val jsonData = callApi( + method = "deezer.pageSearch", + params = mapOf( + "nb" to 128, + "query" to query, + "start" to 0 + ) + ) + val jObject = json.decodeFromString(jsonData) + return jObject + } + + suspend fun searchSuggestions(query: String): JsonObject { + val jsonData = callApi( + method = "search_getSuggestedQueries", + params = mapOf( + "QUERY" to query + ) + ) + val jObject = json.decodeFromString(jsonData) + return jObject + } + suspend fun track(track: Track): JsonObject { val jsonData = callApi( method = "song.getListData", @@ -246,6 +261,18 @@ class DeezerApi( return jObject } + suspend fun artist(artist: Artist): JsonObject { + val jsonData = callApi( + method = "song.getListData", + params = mapOf( + "art_id" to artist.id, + "lang" to settings.deezerLanguage + ) + ) + val jObject = json.decodeFromString(jsonData) + return jObject + } + suspend fun album(album: Album): JsonObject { val jsonData = callApi( method = "deezer.pageAlbum", @@ -304,7 +331,7 @@ class DeezerApi( val jsonData = callApi( method = "page.get", gatewayInput = """ - {"PAGE":"home","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"message":["call_onboarding"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"en","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} + {"PAGE":"home","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"en","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} """.trimIndent() ) val jObject = json.decodeFromString(jsonData) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt index 6083e8b..7fdb58f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt @@ -6,6 +6,7 @@ import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.HomeFeedClient import dev.brahmkshatriya.echo.common.clients.LoginClient import dev.brahmkshatriya.echo.common.clients.PlaylistClient +import dev.brahmkshatriya.echo.common.clients.SearchClient import dev.brahmkshatriya.echo.common.clients.TrackClient import dev.brahmkshatriya.echo.common.exceptions.LoginRequiredException import dev.brahmkshatriya.echo.common.helpers.PagedData @@ -13,38 +14,33 @@ import dev.brahmkshatriya.echo.common.models.Album import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem import dev.brahmkshatriya.echo.common.models.MediaItemsContainer import dev.brahmkshatriya.echo.common.models.Playlist -import dev.brahmkshatriya.echo.common.models.Request +import dev.brahmkshatriya.echo.common.models.QuickSearchItem import dev.brahmkshatriya.echo.common.models.Request.Companion.toRequest import dev.brahmkshatriya.echo.common.models.Streamable import dev.brahmkshatriya.echo.common.models.StreamableAudio -import dev.brahmkshatriya.echo.common.models.StreamableVideo import dev.brahmkshatriya.echo.common.models.Tab import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.common.models.User import dev.brahmkshatriya.echo.common.settings.Setting import dev.brahmkshatriya.echo.common.settings.Settings -import kotlinx.coroutines.DelicateCoroutinesApi +import dev.brahmkshatriya.echo.extension.Utils.getContentLength import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.OkHttpClient -import java.io.ByteArrayInputStream +import okhttp3.Request +import org.apache.http.conn.ConnectTimeoutException import java.io.ByteArrayOutputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec +import java.util.Locale -class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, AlbumClient, PlaylistClient, LoginClient.WebView { +class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, SearchClient, AlbumClient, PlaylistClient, LoginClient.WebView { private val json = Json { isLenient = true @@ -104,57 +100,136 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, AlbumClien override fun getHomeFeed(tab: Tab?): PagedData = PagedData.Single { val dataList = mutableListOf() val jsonData = json.decodeFromString(tab?.extras!!["sections"].toString()) - //val data = jsonData[2].jsonObject.toMediaItemsContainer(name = name) - jsonData.mapIndexed { index, section -> - if(index == 1 || index == 2 || index == 6) { - val name = section.jsonObject["title"]!!.jsonPrimitive.content + jsonData.map { section -> + val name = section.jsonObject["title"]!!.jsonPrimitive.content + // Just for the time being until everything is implemented + if (name == "Continue streaming" || name == "Mixes inspired by..." || name == "Playlists you'll love") { val data = section.jsonObject.toMediaItemsContainer(name = name) dataList.add(data) } } dataList } + + //<============= Search =============> + + override suspend fun quickSearch(query: String?) = query?.run { + try { + val jsonObject = DeezerApi(arl!!, sid!!, token!!, userId!!).searchSuggestions(query) + val resultObject = jsonObject["results"]!!.jsonObject + val suggestionArray = resultObject["SUGGESTION"]!!.jsonArray + suggestionArray.map { item -> + val queryItem = item.jsonObject["QUERY"]!!.jsonPrimitive.content + QuickSearchItem.SearchQueryItem(queryItem, false) + } + } catch (e: NullPointerException) { + null + } catch (e: ConnectTimeoutException) { + null + } + } ?: listOf() + + private var oldSearch: Pair>? = null + override fun searchFeed(query: String?, tab: Tab?) = PagedData.Single { + query ?: return@Single emptyList() + val old = oldSearch?.takeIf { + it.first == query && (tab == null || tab.id == "All") + }?.second + if (old != null) return@Single old + + var list = listOf() + if(tab?.id != "TOP_RESULT") { + val jsonObject = DeezerApi(arl!!, sid!!, token!!, userId!!).search(query) + val resultObject = jsonObject["results"]!!.jsonObject + val tabObject = resultObject[tab?.id]!!.jsonObject + val dataArray = tabObject["data"]!!.jsonArray + + val itemArray = dataArray.mapNotNull { item -> + item.toEchoMediaItem(DeezerApi(arl!!, sid!!, token!!, userId!!))?.toMediaItemsContainer() + } + list = itemArray + } + list + } + + override suspend fun searchTabs(query: String?): List { + query ?: return emptyList() + val jsonObject = DeezerApi(arl!!, sid!!, token!!, userId!!).search(query) + val resultObject = jsonObject["results"]!!.jsonObject + val orderObject = resultObject["ORDER"]!!.jsonArray + + val tabs = orderObject.mapNotNull { + val tab = it.jsonPrimitive.content + Tab( + id = tab, + name = tab.lowercase().capitalize(Locale.ROOT) + ) + }.filter { + it.id != "TOP_RESULT" && + it.id != "FLOW_CONFIG" + } + + oldSearch = query to tabs.map { tab -> + val name = tab.id + Log.d("saerchTabs", name) + val tabObject = resultObject[name]!!.jsonObject + val dataArray = tabObject["data"]!!.jsonArray + dataArray.toMediaItemsContainer(DeezerApi(arl!!, sid!!, token!!, userId!!), name.lowercase().capitalize( + Locale.ROOT)) + } + return listOf(Tab("All", "All")) + tabs + } + //<============= Play =============> private val client = OkHttpClient() override suspend fun getStreamableAudio(streamable: Streamable) = getByteStreamAudio(streamable) + private fun getByteStreamAudio(streamable: Streamable): StreamableAudio { val url = streamable.id - val contentLength = getContentLength(url) + val contentLength = getContentLength(url, client) val key = streamable.extra["key"]!! - val request = okhttp3.Request.Builder().url(url).build() + val request = Request.Builder().url(url).build() var decChunk = ByteArray(0) - with(client.newCall(request).execute()) { - val byteStream = this.body?.byteStream() - - // Read the entire byte stream into memory - val completeStream = ByteArrayOutputStream() - val buffer = ByteArray(16384) - var bytesRead: Int - while (byteStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) { - completeStream.write(buffer, 0, bytesRead) - } - - // Decrypt the complete stream - val completeStreamBytes = completeStream.toByteArray() - var place = 0 + runBlocking { + withContext(Dispatchers.IO) { + val response = client.newCall(request).execute() + val byteStream = response.body?.byteStream() + + // Read the entire byte stream into memory + val completeStream = ByteArrayOutputStream() + val buffer = ByteArray(256 * 1024) + var bytesRead: Int + while (byteStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) { + completeStream.write(buffer, 0, bytesRead) + } - while (place < completeStreamBytes.size) { - val remainingBytes = completeStreamBytes.size - place - val chunkSize = if (remainingBytes > 2048 * 3) 2048 * 3 else remainingBytes - val decryptingChunk = completeStreamBytes.copyOfRange(place, place + chunkSize) - place += chunkSize + // Ensure complete stream is read + val completeStreamBytes = completeStream.toByteArray() + println("Total bytes read: ${completeStreamBytes.size}") + + // Determine chunk size based on decryption block size + val decryptionBlockSize = 2048 * 3 + val numChunks = (completeStreamBytes.size + decryptionBlockSize - 1) / decryptionBlockSize + println("Number of chunks: $numChunks") + + // Decrypt the chunks concurrently + val deferredChunks = (0 until numChunks).map { i -> + val start = i * decryptionBlockSize + val end = minOf((i + 1) * decryptionBlockSize, completeStreamBytes.size) + println("Chunk $i: start $start, end $end") + async { decryptStreamChunk(completeStreamBytes.copyOfRange(start, end), key) } + } - var decryptedChunk = ByteArray(0) - if (decryptingChunk.size > 2048) { - decryptedChunk = Utils.decryptBlowfish(decryptingChunk.copyOfRange(0, 2048), key) - decryptedChunk += decryptingChunk.copyOfRange(2048, decryptingChunk.size) + // Wait for all decryption tasks to complete and concatenate the results + deferredChunks.forEach { deferred -> + decChunk += deferred.await() } - decChunk += decryptedChunk + response.close() } } @@ -164,19 +239,31 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, AlbumClien ) } + private fun decryptStreamChunk(chunk: ByteArray, key: String): ByteArray { + val decryptedStream = ByteArrayOutputStream() + var place = 0 + + while (place < chunk.size) { + val remainingBytes = chunk.size - place + val currentChunkSize = if (remainingBytes > 2048 * 3) 2048 * 3 else remainingBytes + val decryptingChunk = chunk.copyOfRange(place, place + currentChunkSize) + place += currentChunkSize + + if (decryptingChunk.size > 2048) { + val decryptedChunk = Utils.decryptBlowfish(decryptingChunk.copyOfRange(0, 2048), key) + decryptedStream.write(decryptedChunk) + decryptedStream.write(decryptingChunk, 2048, decryptingChunk.size - 2048) + } else { + decryptedStream.write(decryptingChunk) + } + } - - private fun getContentLength(url: String): Long { - var totalLength = 0L - val request = okhttp3.Request.Builder().url(url).head().build() - val response = client.newCall(request).execute() - totalLength += response.header("Content-Length")?.toLong() ?: 0L - response.close() - return totalLength + val decryptedBytes = decryptedStream.toByteArray() + println("Decrypted chunk size: ${decryptedBytes.size}") + return decryptedBytes } - override suspend fun getStreamableVideo(streamable: Streamable) = - StreamableVideo(Request(streamable.id), looping = false, crop = false) + override suspend fun getStreamableVideo(streamable: Streamable) = throw Exception("not Used") override suspend fun loadTrack(track: Track) = coroutineScope { val jsonObject = DeezerApi(arl!!, sid!!, token!!, userId!!, licenseToken!!).getMediaUrl(track) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt b/app/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt index 7af91bb..ccec926 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt @@ -1,10 +1,7 @@ package dev.brahmkshatriya.echo.extension -import android.webkit.URLUtil -import java.io.IOException +import okhttp3.OkHttpClient import java.math.BigInteger -import java.net.HttpURLConnection -import java.net.URL import java.security.MessageDigest import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -14,19 +11,6 @@ object Utils { private const val secret = "g4el58wc0zvf9na1" private val secretIvSpec = IvParameterSpec(byteArrayOf(0,1,2,3,4,5,6,7)) - @Throws(IOException::class) - fun getFinalURL(url: String): String { - val con: HttpURLConnection = URL(url).openConnection() as HttpURLConnection - con.instanceFollowRedirects = false - con.connect() - con.inputStream - if (con.responseCode == HttpURLConnection.HTTP_MOVED_PERM || con.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) { - val redirectUrl: String = con.getHeaderField("Location") - if (URLUtil.isValidUrl(redirectUrl)) return getFinalURL(redirectUrl) - } - return url - } - private fun bitwiseXor(firstVal: Char, secondVal: Char, thirdVal: Char): Char { return (BigInteger(byteArrayOf(firstVal.code.toByte())) xor BigInteger(byteArrayOf(secondVal.code.toByte())) xor @@ -52,15 +36,13 @@ object Utils { return thisTrackCipher.update(chunk) } - fun decryptBlowfishFinal(chunk: ByteArray, blowfishKey: String, isFinal: Boolean): ByteArray { - val secretKeySpec = SecretKeySpec(blowfishKey.toByteArray(), "Blowfish") - val thisTrackCipher = Cipher.getInstance("BLOWFISH/CBC/NoPadding") - thisTrackCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, secretIvSpec) - return if (isFinal) { - thisTrackCipher.doFinal(chunk) - } else { - thisTrackCipher.update(chunk) - } + fun getContentLength(url: String, client: OkHttpClient): Long { + var totalLength = 0L + val request = okhttp3.Request.Builder().url(url).head().build() + val response = client.newCall(request).execute() + totalLength += response.header("Content-Length")?.toLong() ?: 0L + response.close() + return totalLength } }