diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 4a88c08c..62c2f962 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -55,6 +55,7 @@ android { targetSdk = Versions.targetSdkVersion versionCode = Versions.versionCode versionName = Versions.versionName + ndkVersion = "21.4.7075529" } buildTypes { getByName("release") { diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 011923bc..5fe71196 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -26,9 +26,9 @@ import org.gradle.kotlin.dsl.getByType object Versions { // App's Version (To be bumped at each update) - const val versionName = "3.5.0" + const val versionName = "3.6.0" - const val versionCode = 27 + const val versionCode = 28 // Android const val minSdkVersion = 21 @@ -78,6 +78,7 @@ val VersionCatalog.kotlinCoroutines get() = findDependency("kotlin-coroutines"). val VersionCatalog.kotlinxSerialization get() = findDependency("kotlinx-serialization-json").get() val VersionCatalog.ktorClientIOS get() = findDependency("ktor-client-ios").get() val VersionCatalog.ktorClientAndroid get() = findDependency("ktor-client-android").get() +val VersionCatalog.ktorClientAndroidOkHttp get() = findDependency("ktor-client-okhttp").get() val VersionCatalog.ktorClientApache get() = findDependency("ktor-client-apache").get() val VersionCatalog.ktorClientJS get() = findDependency("ktor-client-js").get() val VersionCatalog.ktorClientCIO get() = findDependency("ktor-client-cio").get() diff --git a/buildSrc/deps.versions.toml b/buildSrc/deps.versions.toml index aad933bc..f61031c8 100644 --- a/buildSrc/deps.versions.toml +++ b/buildSrc/deps.versions.toml @@ -58,6 +58,7 @@ ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" } ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" } ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } +ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } ktor-client-curl = { group = "io.ktor", name = "ktor-client-curl", version.ref = "ktor" } ktor-client-apache = { group = "io.ktor", name = "ktor-client-apache", version.ref = "ktor" } ktor-client-ios = { group = "io.ktor", name = "ktor-client-ios", version.ref = "ktor" } diff --git a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts index 54b92e72..ea341621 100644 --- a/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-setup.gradle.kts @@ -67,7 +67,7 @@ kotlin { implementation(compose.materialIconsExtended) implementation(Deps.androidXCommonBundle) implementation(Deps.decomposeComposeExt) - implementation(Deps.ktorClientAndroid) + implementation(Deps.ktorClientAndroidOkHttp) implementation(Deps.koinAndroidBundle) } } diff --git a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt index 1b30fc38..5baa52ff 100644 --- a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt +++ b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/file_manager/AndroidFileManager.kt @@ -210,7 +210,7 @@ class AndroidFileManager( // Get Memory Efficient Bitmap val bitmap: Bitmap? = getMemoryEfficientBitmap(input, reqWidth, reqHeight) - parallelExecutor.executeSuspending { + parallelExecutor.execute { // Decode and Cache Full Sized Image in Background cacheImage( BitmapFactory.decodeByteArray(input, 0, input.size), diff --git a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/media_converter/AudioTagging.kt b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/media_converter/AudioTagging.kt index eafc346f..319ee8af 100644 --- a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/media_converter/AudioTagging.kt +++ b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/media_converter/AudioTagging.kt @@ -40,7 +40,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File { title = track.title album = track.albumName year = track.year - comment = "Genres:${track.comment}" + comment = "${track.comment}" if (track.trackNumber != null) this.track = track.trackNumber.toString() } diff --git a/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/utils/AndroidHttpClient.kt b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/utils/AndroidHttpClient.kt new file mode 100644 index 00000000..92446a8c --- /dev/null +++ b/common/core-components/src/androidMain/kotlin/com/shabinder/common/core_components/utils/AndroidHttpClient.kt @@ -0,0 +1,55 @@ +package com.shabinder.common.core_components.utils + +import android.annotation.SuppressLint +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.HttpClientEngineConfig +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.engine.okhttp.OkHttpConfig +import okhttp3.OkHttpClient +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + +actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient { + return HttpClient(OkHttp) { + engine { + preconfigured = getUnsafeOkHttpClient() + } + extraConfig() + } +} + +fun getUnsafeOkHttpClient(): OkHttpClient { + return try { + // Create a trust manager that does not validate certificate chains + @SuppressLint("CustomX509TrustManager") + val trustAllCerts: TrustManager = object : X509TrustManager { + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted(chain: Array?, authType: String?) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted(chain: Array?, authType: String?) { + } + + override fun getAcceptedIssuers(): Array = arrayOf() + } + + // Install the all-trusting trust manager + val sslContext: SSLContext = SSLContext.getInstance("SSL").apply { + init(null, arrayOf(trustAllCerts), SecureRandom()) + } + + OkHttpClient.Builder().run { + sslSocketFactory(sslContext.socketFactory, trustAllCerts as X509TrustManager) + hostnameVerifier { _, _ -> true } + followRedirects(true) + build() + } + } catch (e: Exception) { + throw RuntimeException(e) + } +} \ No newline at end of file diff --git a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/parallel_executor/ParallelExecutor.kt b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/parallel_executor/ParallelExecutor.kt index 923859c9..05e64cda 100644 --- a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/parallel_executor/ParallelExecutor.kt +++ b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/parallel_executor/ParallelExecutor.kt @@ -40,15 +40,6 @@ interface ParallelProcessor { } } - suspend fun executeSafelyInPool( - onComplete: suspend (result: SuspendableEvent) -> Unit = {}, - block: suspend () -> T - ): SuspendableEvent { - return SuspendableEvent { - parallelExecutor.executeSuspending(block) - }.also { onComplete(it) } - } - suspend fun stopAllTasks() { parallelExecutor.closeAndReInit() } diff --git a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt index ec3e78fd..4d6c8f67 100644 --- a/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt +++ b/common/core-components/src/commonMain/kotlin/com.shabinder.common.core_components/utils/NetworkingExt.kt @@ -2,14 +2,20 @@ package com.shabinder.common.core_components.utils import com.shabinder.common.models.dispatcherIO import com.shabinder.common.utils.globalJson -import io.ktor.client.* -import io.ktor.client.features.* -import io.ktor.client.features.json.* -import io.ktor.client.features.json.serializer.* -import io.ktor.client.features.logging.* -import io.ktor.client.request.* +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.features.HttpTimeout +import io.ktor.client.features.json.JsonFeature +import io.ktor.client.features.json.serializer.KotlinxSerializer +import io.ktor.client.features.logging.DEFAULT +import io.ktor.client.features.logging.LogLevel +import io.ktor.client.features.logging.Logger +import io.ktor.client.features.logging.Logging +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.request.get +import io.ktor.client.request.head import io.ktor.client.statement.HttpResponse -import kotlinx.coroutines.Dispatchers +import io.ktor.http.ContentType import kotlinx.coroutines.withContext import kotlin.native.concurrent.SharedImmutable @@ -32,39 +38,42 @@ suspend inline fun HttpClient.getFinalUrl( ): String { return withContext(dispatcherIO) { runCatching { - get(url,block).call.request.url.toString() + get(url, block).call.request.url.toString() }.getOrNull() ?: url } } fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { - // https://github.com/Kotlin/kotlinx.serialization/issues/1450 - install(JsonFeature) { - serializer = KotlinxSerializer(globalJson) - } - install(HttpTimeout) { - socketTimeoutMillis = 520_000 - requestTimeoutMillis = 360_000 - connectTimeoutMillis = 360_000 - } - // WorkAround for Freezing - // Use httpClient.getData / httpClient.postData Extensions - /*install(JsonFeature) { - serializer = KotlinxSerializer( - Json { - isLenient = true - ignoreUnknownKeys = true + buildHttpClient { + // https://github.com/Kotlin/kotlinx.serialization/issues/1450 + install(JsonFeature) { + serializer = KotlinxSerializer(globalJson) + } + install(HttpTimeout) { + socketTimeoutMillis = 520_000 + requestTimeoutMillis = 360_000 + connectTimeoutMillis = 360_000 + } + // WorkAround for Freezing + // Use httpClient.getData / httpClient.postData Extensions + /*install(JsonFeature) { + serializer = KotlinxSerializer( + Json { + isLenient = true + ignoreUnknownKeys = true + } + ) + }*/ + if (enableNetworkLogs) { + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.INFO } - ) - }*/ - if (enableNetworkLogs) { - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.INFO } } } +expect fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient /*Client Active Throughout App's Lifetime*/ @SharedImmutable diff --git a/common/core-components/src/desktopMain/kotlin/com/shabinder/common/core_components/utils/DesktopHttpClient.kt b/common/core-components/src/desktopMain/kotlin/com/shabinder/common/core_components/utils/DesktopHttpClient.kt new file mode 100644 index 00000000..598e6dfc --- /dev/null +++ b/common/core-components/src/desktopMain/kotlin/com/shabinder/common/core_components/utils/DesktopHttpClient.kt @@ -0,0 +1,25 @@ +package com.shabinder.common.core_components.utils + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.apache.Apache +import org.apache.http.conn.ssl.NoopHostnameVerifier +import org.apache.http.conn.ssl.TrustSelfSignedStrategy +import org.apache.http.ssl.SSLContextBuilder + +actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient { + return HttpClient(Apache) { + engine { + customizeClient { + setSSLContext( + SSLContextBuilder + .create() + .loadTrustMaterial(TrustSelfSignedStrategy()) + .build() + ) + setSSLHostnameVerifier(NoopHostnameVerifier()) + } + } + extraConfig() + } +} diff --git a/common/core-components/src/jsMain/kotlin/com.shabinder.common.core_components/utils/WebHttpClient.kt b/common/core-components/src/jsMain/kotlin/com.shabinder.common.core_components/utils/WebHttpClient.kt new file mode 100644 index 00000000..eed5b458 --- /dev/null +++ b/common/core-components/src/jsMain/kotlin/com.shabinder.common.core_components/utils/WebHttpClient.kt @@ -0,0 +1,9 @@ +package com.shabinder.common.core_components.utils + +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.js.Js + +actual fun buildHttpClient(extraConfig: HttpClientConfig<*>.() -> Unit): HttpClient = HttpClient(Js) { + extraConfig() +} diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt index 78593b2d..fcd7162a 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/soundcloud/resolvemodel/SoundCloudResolveResponseBase.kt @@ -31,7 +31,7 @@ sealed class SoundCloudResolveResponseBase { @SerialName("embeddable_by") val embeddableBy: String = "", val genre: String = "", - val id: Int = 0, + val id: String = "", @SerialName("is_album") val isAlbum: Boolean = false, @SerialName("label_name") @@ -100,7 +100,7 @@ sealed class SoundCloudResolveResponseBase { val genre: String = "", @SerialName("has_downloads_left") val hasDownloadsLeft: Boolean = false, - val id: Int = 0, + val id: String = "", override val kind: String = "", @SerialName("label_name") val labelName: String = "", diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt index cb685184..4a161569 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/utils/Utils.kt @@ -16,5 +16,5 @@ val globalJson by lazy { * Removing Illegal Chars from File Name * **/ fun removeIllegalChars(fileName: String): String { - return fileName.replace("[^\\dA-Za-z_]".toRegex(), "_") + return fileName.replace("[^\\dA-Za-z0-9-_]".toRegex(), "_") } diff --git a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt index 004e49b4..7e71fee0 100644 --- a/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt +++ b/common/providers/src/commonMain/kotlin/com.shabinder.common.providers/sound_cloud/requests/SoundCloudRequests.kt @@ -5,6 +5,7 @@ import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack +import com.shabinder.common.utils.globalJson import io.ktor.client.HttpClient import io.ktor.client.features.ClientRequestException import io.ktor.client.request.get @@ -12,7 +13,13 @@ import io.ktor.client.request.parameter import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.supervisorScope +import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive interface SoundCloudRequests { @@ -74,18 +81,21 @@ interface SoundCloudRequests { if (media.transcodings.isNotEmpty()) return this - val infoURL = URLS.TRACK_INFO.buildURL(id.toString()) - return httpClient.get(infoURL) { + val infoURL = URLS.TRACK_INFO.buildURL(id) + + val data: String = httpClient.get(infoURL) { parameter("client_id", CLIENT_ID) } + return globalJson.decodeFromString(data) } private suspend fun getResponseObj(url: String, clientID: String = CLIENT_ID): SoundCloudResolveResponseBase { val itemURL = URLS.RESOLVE.buildURL(url) val resp: SoundCloudResolveResponseBase = try { - httpClient.get(itemURL) { + val data: String = httpClient.get(itemURL) { parameter("client_id", clientID) } + globalJson.decodeFromString(SoundCloudSerializer, data) } catch (e: ClientRequestException) { if (clientID != ALT_CLIENT_ID) return getResponseObj(url, ALT_CLIENT_ID) @@ -116,6 +126,25 @@ interface SoundCloudRequests { const val CLIENT_ID = "a3e059563d7fd3372b49b37f00a00bcf" const val ALT_CLIENT_ID = "2t9loNQH90kzJcsFCODdigxfp325aq4z" + + object SoundCloudSerializer : + JsonContentPolymorphicSerializer(SoundCloudResolveResponseBase::class) { + override fun selectDeserializer(element: JsonElement): DeserializationStrategy = + when { + "track_count" in element.jsonObject -> SoundCloudResolveResponsePlaylist.serializer() + "kind" in element.jsonObject -> { + val isTrack = + element.jsonObject["kind"] + ?.jsonPrimitive?.content.toString() + .contains("track", true) + when { + isTrack || "track_format" in element.jsonObject -> SoundCloudResolveResponseTrack.serializer() + else -> SoundCloudResolveResponsePlaylist.serializer() + } + } + else -> SoundCloudResolveResponsePlaylist.serializer() + } + } } }