From 581f4d0104a390ce83f04c41349a03cc22f1d46a Mon Sep 17 00:00:00 2001 From: shabinder Date: Sun, 20 Jun 2021 02:16:59 +0530 Subject: [PATCH 01/15] Console App --- console-app/src/main/java/common/Common.kt | 29 +++++++++++++++++++ .../src/main/java/common/Parameters.kt | 29 +++++++++++++++++++ desktop/build.gradle.kts | 2 +- settings.gradle.kts | 1 + web-app/src/main/kotlin/App.kt | 9 +++--- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 console-app/src/main/java/common/Common.kt create mode 100644 console-app/src/main/java/common/Parameters.kt diff --git a/console-app/src/main/java/common/Common.kt b/console-app/src/main/java/common/Common.kt new file mode 100644 index 00000000..0ceb044f --- /dev/null +++ b/console-app/src/main/java/common/Common.kt @@ -0,0 +1,29 @@ +@file:Suppress("FunctionName") + +package common + +import io.ktor.client.HttpClient +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 kotlinx.serialization.json.Json + +internal val client = HttpClient { + install(HttpTimeout) + install(JsonFeature) { + serializer = KotlinxSerializer( + Json { + ignoreUnknownKeys = true + isLenient = true + } + ) + } + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.INFO + } +} diff --git a/console-app/src/main/java/common/Parameters.kt b/console-app/src/main/java/common/Parameters.kt new file mode 100644 index 00000000..1f1f2623 --- /dev/null +++ b/console-app/src/main/java/common/Parameters.kt @@ -0,0 +1,29 @@ +package common + +import utils.byOptionalProperty +import utils.byProperty + +internal data class Parameters( + val githubToken: String, + val ownerName: String, + val repoName: String, + val branchName: String, + val filePath: String, + val imageDescription: String, + val commitMessage: String, + val tagName: String +) { + companion object { + fun initParameters() = Parameters( + githubToken = "GH_TOKEN".byProperty, + ownerName = "OWNER_NAME".byProperty, + repoName = "REPO_NAME".byProperty, + branchName = "BRANCH_NAME".byOptionalProperty ?: "main", + filePath = "FILE_PATH".byOptionalProperty ?: "README.md", + imageDescription = "IMAGE_DESCRIPTION".byOptionalProperty ?: "IMAGE", + commitMessage = "COMMIT_MESSAGE".byOptionalProperty ?: "HTML-TO-IMAGE Update", + tagName = "TAG_NAME".byOptionalProperty ?: "HTI" + // hctiKey = "HCTI_KEY".analytics_html_img.getByProperty + ) + } +} diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 994a3e28..07bf0a0d 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -38,8 +38,8 @@ kotlin { implementation(compose.desktop.currentOs) implementation(project(":common:database")) implementation(project(":common:dependency-injection")) - implementation(project(":common:compose")) implementation(project(":common:data-models")) + implementation(project(":common:compose")) implementation(project(":common:root")) // Decompose diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c54a1fa..8cf289b8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,5 +27,6 @@ include( ":android", ":desktop", ":web-app", + ":console-app", ":maintenance-tasks" ) diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt index 4614b56f..03d1ed52 100644 --- a/web-app/src/main/kotlin/App.kt +++ b/web-app/src/main/kotlin/App.kt @@ -42,7 +42,6 @@ fun RBuilder.App(attrs: AppProps.() -> Unit): ReactElement { } } - @Suppress("EXPERIMENTAL_IS_NOT_ENABLED", "NON_EXPORTABLE_TYPE") @OptIn(ExperimentalJsExport::class) @JsExport @@ -52,6 +51,10 @@ class App(props: AppProps): RComponent(props) { private val ctx = DefaultComponentContext(lifecycle = lifecycle) private val dependencies = props.dependencies + override fun RBuilder.render() { + renderableChild(RootR::class, root) + } + private val root = SpotiFlyerRoot(ctx, object : SpotiFlyerRoot.Dependencies { override val storeFactory: StoreFactory = LoggingStoreFactory(DefaultStoreFactory) @@ -107,8 +110,4 @@ class App(props: AppProps): RComponent(props) { override fun componentWillUnmount() { lifecycle.destroy() } - - override fun RBuilder.render() { - renderableChild(RootR::class, root) - } } \ No newline at end of file From 0b7b93ba63f77723153efab1caf8df7f628864da Mon Sep 17 00:00:00 2001 From: shabinder Date: Mon, 21 Jun 2021 00:44:47 +0530 Subject: [PATCH 02/15] Better Error Handling and Major Code Cleanup --- common/data-models/build.gradle.kts | 1 + .../kotlin/com/shabinder/common/Ext.kt | 3 + .../shabinder/common/models/AllPlatforms.kt | 23 -- .../com/shabinder/common/models/CorsProxy.kt | 5 + .../common/models/SpotiFlyerException.kt | 21 ++ .../shabinder/common/models/event/Event.kt | 202 +++++++++++ .../shabinder/common/models/event/Factory.kt | 17 + .../common/models/event/Validation.kt | 8 + .../event/coroutines/SuspendableEvent.kt | 159 +++++++++ .../event/coroutines/SuspendedValidation.kt | 9 + .../com/shabinder/common/di/AndroidActual.kt | 4 - .../kotlin/com/shabinder/common/di/DI.kt | 15 +- .../kotlin/com/shabinder/common/di/Expect.kt | 7 +- .../common/di/gaana/GaanaRequests.kt | 12 +- .../common/di/providers/GaanaProvider.kt | 23 +- .../common/di/providers/SaavnProvider.kt | 15 +- .../common/di/providers/SpotifyProvider.kt | 46 ++- .../common/di/providers/YoutubeMp3.kt | 38 +-- .../common/di/providers/YoutubeMusic.kt | 314 +++++++++--------- .../common/di/providers/YoutubeProvider.kt | 182 +++++----- .../common/di/saavn/JioSaavnRequests.kt | 17 +- .../common/di/spotify/SpotifyAuth.kt | 23 +- .../common/di/spotify/SpotifyRequests.kt | 10 +- .../shabinder/common/di/youtubeMp3/Yt1sMp3.kt | 33 +- .../com/shabinder/common/di/DesktopActual.kt | 4 - .../com/shabinder/common/di/WebActual.kt | 4 - .../root/integration/SpotiFlyerRootImpl.kt | 11 +- 27 files changed, 786 insertions(+), 420 deletions(-) create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt delete mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts index 65775562..5f8dd925 100644 --- a/common/data-models/build.gradle.kts +++ b/common/data-models/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { implementation("co.touchlab:stately-concurrency:$statelyVersion") implementation("co.touchlab:stately-isolate:$statelyIsoVersion") implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion") + implementation(Extras.youtubeDownloader) } } androidMain { diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt new file mode 100644 index 00000000..e8f24aea --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt @@ -0,0 +1,3 @@ +package com.shabinder.common + +fun T?.requireNotNull() : T = requireNotNull(this) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt deleted file mode 100644 index 14f894f1..00000000 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AllPlatforms.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * * Copyright (c) 2021 Shabinder Singh - * * This program is free software: you can redistribute it and/or modify - * * it under the terms of the GNU General Public License as published by - * * the Free Software Foundation, either version 3 of the License, or - * * (at your option) any later version. - * * - * * This program is distributed in the hope that it will be useful, - * * but WITHOUT ANY WARRANTY; without even the implied warranty of - * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * * GNU General Public License for more details. - * * - * * You should have received a copy of the GNU General Public License - * * along with this program. If not, see . - */ - -package com.shabinder.common.models - -sealed class AllPlatforms { - object Js : AllPlatforms() - object Jvm : AllPlatforms() - object Native : AllPlatforms() -} diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt index 75a9ee21..40e57a12 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt @@ -16,6 +16,9 @@ package com.shabinder.common.models +import io.github.shabinder.TargetPlatforms +import io.github.shabinder.activePlatform + sealed class CorsProxy(open val url: String) { data class SelfHostedCorsProxy(override val url: String = "https://cors.spotiflyer.ml/cors/" /*"https://spotiflyer.azurewebsites.net/"*/) : CorsProxy(url) data class PublicProxyWithExtension(override val url: String = "https://cors.bridged.cc/") : CorsProxy(url) @@ -45,3 +48,5 @@ sealed class CorsProxy(open val url: String) { * Default Self Hosted, However ask user to use extension if possible. * */ var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy() + +val corsApi get() = if (activePlatform is TargetPlatforms.Js) corsProxy.url else "" \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt new file mode 100644 index 00000000..472724e2 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -0,0 +1,21 @@ +package com.shabinder.common.models + +sealed class SpotiFlyerException(override val message: String): Exception(message) { + + data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) + + data class NoMatchFound( + val trackName: String? = null, + override val message: String = "$trackName : NO Match Found!" + ): SpotiFlyerException(message) + + data class YoutubeLinkNotFound( + val videoID: String? = null, + override val message: String = "No Downloadable link found for videoID: $videoID" + ): SpotiFlyerException(message) + + data class LinkInvalid( + val link: String? = null, + override val message: String = "Entered Link is NOT Valid!\n ${link ?: ""}" + ): SpotiFlyerException(message) +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt new file mode 100644 index 00000000..e3d6621d --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt @@ -0,0 +1,202 @@ +package com.shabinder.common.models.event + +inline fun Event<*, *>.getAs() = when (this) { + is Event.Success -> value as? X + is Event.Failure -> error as? X +} + +inline fun Event.success(f: (V) -> Unit) = fold(f, {}) + +inline fun Event<*, E>.failure(f: (E) -> Unit) = fold({}, f) + +infix fun Event.or(fallback: V) = when (this) { + is Event.Success -> this + else -> Event.Success(fallback) +} + +inline infix fun Event.getOrElse(fallback: (E) -> V): V { + return when (this) { + is Event.Success -> value + is Event.Failure -> fallback(error) + } +} + +fun Event.getOrNull(): V? { + return when (this) { + is Event.Success -> value + is Event.Failure -> null + } +} + +fun Event.getThrowableOrNull(): E? { + return when (this) { + is Event.Success -> null + is Event.Failure -> error + } +} + +inline fun Event.mapEither( + success: (V) -> U, + failure: (E) -> F +): Event { + return when (this) { + is Event.Success -> Event.success(success(value)) + is Event.Failure -> Event.error(failure(error)) + } +} + +inline fun Event.map(transform: (V) -> U): Event = try { + when (this) { + is Event.Success -> Event.Success(transform(value)) + is Event.Failure -> Event.Failure(error) + } +} catch (ex: Throwable) { + when (ex) { + is E -> Event.error(ex) + else -> throw ex + } +} + +inline fun Event.flatMap(transform: (V) -> Event): Event = + try { + when (this) { + is Event.Success -> transform(value) + is Event.Failure -> Event.Failure(error) + } + } catch (ex: Throwable) { + when (ex) { + is E -> Event.error(ex) + else -> throw ex + } + } + +inline fun Event.mapError(transform: (E) -> E2) = when (this) { + is Event.Success -> Event.Success(value) + is Event.Failure -> Event.Failure(transform(error)) +} + +inline fun Event.flatMapError(transform: (E) -> Event) = + when (this) { + is Event.Success -> Event.Success(value) + is Event.Failure -> transform(error) + } + +inline fun Event.onError(f: (E) -> Unit) = when (this) { + is Event.Success -> Event.Success(value) + is Event.Failure -> { + f(error) + this + } +} + +inline fun Event.onSuccess(f: (V) -> Unit): Event { + return when (this) { + is Event.Success -> { + f(value) + this + } + is Event.Failure -> this + } +} + +inline fun Event.any(predicate: (V) -> Boolean): Boolean = try { + when (this) { + is Event.Success -> predicate(value) + is Event.Failure -> false + } +} catch (ex: Throwable) { + false +} + +inline fun Event.fanout(other: () -> Event): Event, *> = + flatMap { outer -> other().map { outer to it } } + +inline fun List>.lift(): Event, E> = fold( + Event.success( + mutableListOf() + ) as Event, E> +) { acc, Event -> + acc.flatMap { combine -> + Event.map { combine.apply { add(it) } } + } +} + +inline fun Event.unwrap(failure: (E) -> Nothing): V = + apply { component2()?.let(failure) }.component1()!! + +inline fun Event.unwrapError(success: (V) -> Nothing): E = + apply { component1()?.let(success) }.component2()!! + + +sealed class Event { + + open operator fun component1(): V? = null + open operator fun component2(): E? = null + + inline fun fold(success: (V) -> X, failure: (E) -> X): X = when (this) { + is Success -> success(this.value) + is Failure -> failure(this.error) + } + + abstract fun get(): V + + class Success(val value: V) : Event() { + override fun component1(): V? = value + + override fun get(): V = value + + override fun toString() = "[Success: $value]" + + override fun hashCode(): Int = value.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Success<*> && value == other.value + } + } + + class Failure(val error: E) : Event() { + override fun component2(): E? = error + + override fun get() = throw error + + fun getThrowable(): E = error + + override fun toString() = "[Failure: $error]" + + override fun hashCode(): Int = error.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Failure<*> && error == other.error + } + } + + companion object { + // Factory methods + fun error(ex: E) = Failure(ex) + + fun success(v: V) = Success(v) + + inline fun of( + value: V?, + fail: (() -> Throwable) = { Throwable() } + ): Event = + value?.let { success(it) } ?: error(fail()) + + inline fun of(crossinline f: () -> V): Event = try { + success(f()) + } catch (ex: Throwable) { + when (ex) { + is E -> error(ex) + else -> throw ex + } + } + + inline operator fun invoke(crossinline f: () -> V): Event = try { + success(f()) + } catch (ex: Throwable) { + error(ex) + } + } +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt new file mode 100644 index 00000000..9d47d6d7 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt @@ -0,0 +1,17 @@ +package com.shabinder.common.models.event + +inline fun runCatching(block: () -> V): Event { + return try { + Event.success(block()) + } catch (e: Throwable) { + Event.error(e) + } +} + +inline infix fun T.runCatching(block: T.() -> V): Event { + return try { + Event.success(block()) + } catch (e: Throwable) { + Event.error(e) + } +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt new file mode 100644 index 00000000..d302c935 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt @@ -0,0 +1,8 @@ +package com.shabinder.common.models.event + +class Validation(vararg resultSequence: Event<*, E>) { + + val failures: List = resultSequence.filterIsInstance>().map { it.getThrowable() } + + val hasFailure = failures.isNotEmpty() +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt new file mode 100644 index 00000000..9e739ef8 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt @@ -0,0 +1,159 @@ +package com.shabinder.common.models.event.coroutines + +inline fun SuspendableEvent<*, *>.getAs() = when (this) { + is SuspendableEvent.Success -> value as? X + is SuspendableEvent.Failure -> error as? X +} + +suspend inline fun SuspendableEvent.success(crossinline f: suspend (V) -> Unit) = fold(f, {}) + +suspend inline fun SuspendableEvent<*, E>.failure(crossinline f: suspend (E) -> Unit) = fold({}, f) + +infix fun SuspendableEvent.or(fallback: V) = when (this) { + is SuspendableEvent.Success -> this + else -> SuspendableEvent.Success(fallback) +} + +suspend inline infix fun SuspendableEvent.getOrElse(crossinline fallback:suspend (E) -> V): V { + return when (this) { + is SuspendableEvent.Success -> value + is SuspendableEvent.Failure -> fallback(error) + } +} + +fun SuspendableEvent.getOrNull(): V? { + return when (this) { + is SuspendableEvent.Success -> value + is SuspendableEvent.Failure -> null + } +} + +suspend inline fun SuspendableEvent.map( + crossinline transform: suspend (V) -> U +): SuspendableEvent = try { + when (this) { + is SuspendableEvent.Success -> SuspendableEvent.Success(transform(value)) + is SuspendableEvent.Failure -> SuspendableEvent.Failure(error) + } +} catch (ex: Throwable) { + SuspendableEvent.error(ex as E) +} + +suspend inline fun SuspendableEvent.flatMap( + crossinline transform: suspend (V) -> SuspendableEvent +): SuspendableEvent = try { + when (this) { + is SuspendableEvent.Success -> transform(value) + is SuspendableEvent.Failure -> SuspendableEvent.Failure(error) + } +} catch (ex: Throwable) { + SuspendableEvent.error(ex as E) +} + +suspend inline fun SuspendableEvent.mapError( + crossinline transform: suspend (E) -> E2 +) = when (this) { + is SuspendableEvent.Success -> SuspendableEvent.Success(value) + is SuspendableEvent.Failure -> SuspendableEvent.Failure(transform(error)) +} + +suspend inline fun SuspendableEvent.flatMapError( + crossinline transform: suspend (E) -> SuspendableEvent +) = when (this) { + is SuspendableEvent.Success -> SuspendableEvent.Success(value) + is SuspendableEvent.Failure -> transform(error) +} + +suspend inline fun SuspendableEvent.any( + crossinline predicate: suspend (V) -> Boolean +): Boolean = try { + when (this) { + is SuspendableEvent.Success -> predicate(value) + is SuspendableEvent.Failure -> false + } +} catch (ex: Throwable) { + false +} + +suspend inline fun SuspendableEvent.fanout( + crossinline other: suspend () -> SuspendableEvent +): SuspendableEvent, *> = + flatMap { outer -> other().map { outer to it } } + + +suspend fun List>.lift(): SuspendableEvent, E> = fold( + SuspendableEvent.Success, E>(mutableListOf()) as SuspendableEvent, E> +) { acc, result -> + acc.flatMap { combine -> + result.map { combine.apply { add(it) } } + } +} + +sealed class SuspendableEvent { + + abstract operator fun component1(): V? + abstract operator fun component2(): E? + + suspend inline fun fold(crossinline success: suspend (V) -> X, crossinline failure: suspend (E) -> X): X { + return when (this) { + is Success -> success(this.value) + is Failure -> failure(this.error) + } + } + + abstract val value: V + + class Success(override val value: V) : SuspendableEvent() { + override fun component1(): V? = value + override fun component2(): E? = null + + override fun toString() = "[Success: $value]" + + override fun hashCode(): Int = value.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Success<*, *> && value == other.value + } + } + + class Failure(val error: E) : SuspendableEvent() { + override fun component1(): V? = null + override fun component2(): E? = error + + override val value: V = throw error + + fun getThrowable(): E = error + + override fun toString() = "[Failure: $error]" + + override fun hashCode(): Int = error.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Failure<*, *> && error == other.error + } + } + + companion object { + // Factory methods + fun error(ex: E) = Failure(ex) + + inline fun of(value: V?,crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent { + return value?.let { Success(it) } ?: error(fail()) + } + + suspend inline fun of(crossinline f: suspend () -> V): SuspendableEvent = try { + Success(f()) + } catch (ex: Throwable) { + Failure(ex as E) + } + + suspend inline operator fun invoke(crossinline f: suspend () -> V): SuspendableEvent = try { + Success(f()) + } catch (ex: Throwable) { + Failure(ex) + } + } + +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt new file mode 100644 index 00000000..ce29ec0d --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt @@ -0,0 +1,9 @@ +package com.shabinder.common.models.event.coroutines + +class SuspendedValidation(vararg resultSequence: SuspendableEvent<*, E>) { + + val failures: List = resultSequence.filterIsInstance>().map { it.getThrowable() } + + val hasFailure = failures.isNotEmpty() + +} \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt index 9514dbf4..3c4bf366 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt @@ -16,7 +16,6 @@ package com.shabinder.common.di -import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods import kotlinx.coroutines.CoroutineDispatcher @@ -25,9 +24,6 @@ import kotlinx.coroutines.Dispatchers // IO-Dispatcher actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO -// Current Platform Info -actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm - actual suspend fun downloadTracks( list: List, fetcher: FetchPlatformQueryResult, diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index 2ed101db..fb531555 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -27,14 +27,11 @@ import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeProvider -import io.ktor.client.HttpClient -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.* +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 kotlinx.serialization.json.Json import org.koin.core.context.startKoin import org.koin.dsl.KoinAppDeclaration @@ -62,7 +59,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module { single { GaanaProvider(get(), get(), get()) } single { SaavnProvider(get(), get(), get(), get()) } single { YoutubeProvider(get(), get(), get()) } - single { YoutubeMp3(get(), get(), get()) } + single { YoutubeMp3(get(), get()) } single { YoutubeMusic(get(), get(), get(), get(), get()) } single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt index 97fb7c46..1d1dfaf6 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt @@ -16,9 +16,8 @@ package com.shabinder.common.di -import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.TrackDetails -import io.ktor.client.request.head +import io.ktor.client.request.* import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -34,10 +33,6 @@ expect suspend fun downloadTracks( @SharedImmutable expect val dispatcherIO: CoroutineDispatcher -// Current Platform Info -@SharedImmutable -expect val currentPlatform: AllPlatforms - suspend fun isInternetAccessible(): Boolean { return withContext(dispatcherIO) { try { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt index 99a232af..11f36a52 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt @@ -16,21 +16,15 @@ package com.shabinder.common.di.gaana -import com.shabinder.common.di.currentPlatform -import com.shabinder.common.models.AllPlatforms -import com.shabinder.common.models.corsProxy +import com.shabinder.common.models.corsApi import com.shabinder.common.models.gaana.GaanaAlbum import com.shabinder.common.models.gaana.GaanaArtistDetails import com.shabinder.common.models.gaana.GaanaArtistTracks import com.shabinder.common.models.gaana.GaanaPlaylist import com.shabinder.common.models.gaana.GaanaSong -import io.ktor.client.HttpClient -import io.ktor.client.request.get +import io.ktor.client.* +import io.ktor.client.request.* -val corsApi get() = if (currentPlatform is AllPlatforms.Js) { - corsProxy.url -} // "https://spotiflyer-cors.azurewebsites.net/" //"https://spotiflyer-cors.herokuapp.com/"//"https://cors.bridged.cc/" -else "" private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990" private val BASE_URL get() = "${corsApi}https://api.gaana.com" diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt index 476ca9b0..5fdfda90 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt @@ -22,10 +22,12 @@ import com.shabinder.common.di.finalOutputDir import com.shabinder.common.di.gaana.GaanaRequests import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.gaana.GaanaTrack import com.shabinder.common.models.spotify.Source -import io.ktor.client.HttpClient +import io.ktor.client.* class GaanaProvider( override val httpClient: HttpClient, @@ -35,7 +37,7 @@ class GaanaProvider( private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" - suspend fun query(fullLink: String): PlatformQueryResult? { + suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent { // Link Schema: https://gaana.com/type/link val gaanaLink = fullLink.substringAfter("gaana.com/") @@ -44,17 +46,13 @@ class GaanaProvider( // Error if (type == "Error" || link == "Error") { - return null - } - return try { - gaanaSearch( - type, - link - ) - } catch (e: Exception) { - e.printStackTrace() - null + throw SpotiFlyerException.LinkInvalid() } + + gaanaSearch( + type, + link + ) } private suspend fun gaanaSearch( @@ -137,6 +135,7 @@ class GaanaProvider( outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/) ) } + private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus { return if (dir.isPresent( dir.finalOutputDir( diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt index 290744b7..ca78e490 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt @@ -8,10 +8,12 @@ import com.shabinder.common.di.saavn.JioSaavnRequests import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.saavn.SaavnSong import com.shabinder.common.models.spotify.Source -import io.ktor.client.HttpClient +import io.ktor.client.* class SaavnProvider( override val httpClient: HttpClient, @@ -20,16 +22,15 @@ class SaavnProvider( private val dir: Dir, ) : JioSaavnRequests { - suspend fun query(fullLink: String): PlatformQueryResult { - val result = PlatformQueryResult( + suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent { + PlatformQueryResult( folderType = "", subFolder = "", title = "", coverUrl = "", trackList = listOf(), Source.JioSaavn - ) - with(result) { + ).apply { when (fullLink.substringAfter("saavn.com/").substringBefore("/")) { "song" -> { getSong(fullLink).let { @@ -59,12 +60,10 @@ class SaavnProvider( } } else -> { - // Handle Error + throw SpotiFlyerException.LinkInvalid(fullLink) } } } - - return result } private fun List.toTrackDetails(type: String, subFolder: String): List = this.map { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index 44b67370..ac85927c 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -27,17 +27,19 @@ import com.shabinder.common.di.spotify.authenticateSpotify import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.NativeAtomicReference import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.spotify.Album import com.shabinder.common.models.spotify.Image import com.shabinder.common.models.spotify.PlaylistTrack import com.shabinder.common.models.spotify.Source import com.shabinder.common.models.spotify.Track -import io.ktor.client.HttpClient -import io.ktor.client.features.defaultRequest -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer -import io.ktor.client.request.header +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.request.* class SpotifyProvider( private val tokenStore: TokenStore, @@ -64,7 +66,7 @@ class SpotifyProvider( override val httpClientRef = NativeAtomicReference(createHttpClient(true)) - suspend fun query(fullLink: String): PlatformQueryResult? { + suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent { var spotifyLink = "https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim() @@ -78,15 +80,16 @@ class SpotifyProvider( val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') if (type == "Error" || link == "Error") { - return null + throw SpotiFlyerException.LinkInvalid(fullLink) } if (type == "episode" || type == "show") { - // TODO Implementation - return null + throw SpotiFlyerException.FeatureNotImplementedYet( + "Support for Spotify's ${type.uppercase()} isn't implemented yet" + ) } - return try { + try { spotifySearch( type, link @@ -95,16 +98,11 @@ class SpotifyProvider( e.printStackTrace() // Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions authenticateSpotifyClient(true) - // Retry Search - try { - spotifySearch( - type, - link - ) - } catch (e: Exception) { - e.printStackTrace() - null - } + + spotifySearch( + type, + link + ) } } @@ -112,15 +110,14 @@ class SpotifyProvider( type: String, link: String ): PlatformQueryResult { - val result = PlatformQueryResult( + return PlatformQueryResult( folderType = "", subFolder = "", title = "", coverUrl = "", trackList = listOf(), Source.Spotify - ) - with(result) { + ).apply { when (type) { "track" -> { getTrack(link).also { @@ -190,11 +187,10 @@ class SpotifyProvider( "show" -> { // TODO } else -> { - // TODO Handle Error + throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link") } } } - return result } /* diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt index 9bd488ea..582f492a 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt @@ -17,28 +17,28 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit -import com.shabinder.common.di.Dir -import com.shabinder.common.di.currentPlatform +import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.youtubeMp3.Yt1sMp3 -import com.shabinder.common.models.AllPlatforms +import com.shabinder.common.models.corsApi +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.map import io.ktor.client.* -class YoutubeMp3( - override val httpClient: HttpClient, - override val logger: Kermit, - private val dir: Dir, -) : Yt1sMp3 { - suspend fun getMp3DownloadLink(videoID: String): String? = try { - logger.i { "Youtube MP3 Link Fetching!" } - getLinkFromYt1sMp3(videoID)?.let { - logger.i { "Download Link: $it" } - if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/) - "https://cors.spotiflyer.ml/cors/$it" - // "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue - else it +interface YoutubeMp3: Yt1sMp3 { + + companion object { + operator fun invoke( + client: HttpClient, + logger: Kermit + ): AudioToMp3 { + return object : AudioToMp3 { + override val client: HttpClient = client + override val logger: Kermit = logger + } } - } catch (e: Exception) { - e.printStackTrace() - null + } + + suspend fun getMp3DownloadLink(videoID: String): SuspendableEvent = getLinkFromYt1sMp3(videoID).map { + corsApi + it } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index 215a5ae5..195f615f 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -18,15 +18,18 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit import com.shabinder.common.di.audioToMp3.AudioToMp3 -import com.shabinder.common.di.gaana.corsApi +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.YoutubeTrack +import com.shabinder.common.models.corsApi +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.flatMap +import com.shabinder.common.models.event.coroutines.flatMapError +import com.shabinder.common.models.event.coroutines.map import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch -import io.ktor.client.HttpClient -import io.ktor.client.request.headers -import io.ktor.client.request.post -import io.ktor.http.ContentType -import io.ktor.http.contentType +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.http.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.buildJsonArray @@ -37,6 +40,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject +import kotlin.collections.set import kotlin.math.absoluteValue class YoutubeMusic constructor( @@ -54,179 +58,178 @@ class YoutubeMusic constructor( suspend fun findSongDownloadURL( trackDetails: TrackDetails - ): String? { + ): SuspendableEvent { val bestMatchVideoID = getYTIDBestMatch(trackDetails) - return bestMatchVideoID?.let { videoID -> - youtubeMp3.getMp3DownloadLink(videoID) ?: youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url?.let { m4aLink -> - audioToMp3.convertToMp3( - m4aLink - ) + return bestMatchVideoID.flatMap { videoID -> + // Get Downloadable Link + youtubeMp3.getMp3DownloadLink(videoID).flatMapError { + SuspendableEvent { + youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink -> + audioToMp3.convertToMp3(m4aLink) + } ?: throw SpotiFlyerException.YoutubeLinkNotFound(videoID) + } } } } - suspend fun getYTIDBestMatch( + private suspend fun getYTIDBestMatch( trackDetails: TrackDetails - ): String? { - return try { + ):SuspendableEvent = + getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList -> sortByBestMatch( - getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}"), + matchList, trackName = trackDetails.title, trackArtists = trackDetails.artists, trackDurationSec = trackDetails.durationSec - ).keys.firstOrNull() - } catch (e: Exception) { - // All Internet/Client Related Errors - e.printStackTrace() - null + ).keys.firstOrNull() ?: throw SpotiFlyerException.NoMatchFound(trackDetails.title) } - } - private suspend fun getYTTracks(query: String): List { - val youtubeTracks = mutableListOf() - val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query)) - logger.i { "Youtube Music Response Recieved" } - val contentBlocks = responseObj.jsonObject["contents"] - ?.jsonObject?.get("sectionListRenderer") - ?.jsonObject?.get("contents")?.jsonArray + private suspend fun getYTTracks(query: String): SuspendableEvent,Throwable> = + getYoutubeMusicResponse(query).map { youtubeResponseData -> + val youtubeTracks = mutableListOf() + val responseObj = Json.parseToJsonElement(youtubeResponseData) + // logger.i { "Youtube Music Response Received" } + val contentBlocks = responseObj.jsonObject["contents"] + ?.jsonObject?.get("sectionListRenderer") + ?.jsonObject?.get("contents")?.jsonArray - val resultBlocks = mutableListOf() - if (contentBlocks != null) { - for (cBlock in contentBlocks) { - /** - *Ignore user-suggestion - *The 'itemSectionRenderer' field is for user notices (stuff like - 'showing - *results for xyz, search for abc instead') we have no use for them, the for - *loop below if throw a keyError if we don't ignore them - */ - if (cBlock.jsonObject.containsKey("itemSectionRenderer")) { - continue - } - - for ( - contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray - ?: listOf() - ) { + val resultBlocks = mutableListOf() + if (contentBlocks != null) { + for (cBlock in contentBlocks) { /** - * apparently content Blocks without an 'overlay' field don't have linkBlocks - * I have no clue what they are and why there even exist - * - if(!contents.containsKey("overlay")){ - println(contents) - continue - TODO check and correct - }*/ - - val result = contents.jsonObject["musicResponsiveListItemRenderer"] - ?.jsonObject?.get("flexColumns")?.jsonArray - - // Add the linkBlock - val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"] - ?.jsonObject?.get("overlay") - ?.jsonObject?.get("musicItemThumbnailOverlayRenderer") - ?.jsonObject?.get("content") - ?.jsonObject?.get("musicPlayButtonRenderer") - ?.jsonObject?.get("playNavigationEndpoint") - - // detailsBlock is always a list, so we just append the linkBlock to it - // instead of carrying along all the other junk from "musicResponsiveListItemRenderer" - val finalResult = buildJsonArray { - result?.let { add(it) } - linkBlock?.let { add(it) } + *Ignore user-suggestion + *The 'itemSectionRenderer' field is for user notices (stuff like - 'showing + *results for xyz, search for abc instead') we have no use for them, the for + *loop below if throw a keyError if we don't ignore them + */ + if (cBlock.jsonObject.containsKey("itemSectionRenderer")) { + continue + } + + for ( + contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray + ?: listOf() + ) { + /** + * apparently content Blocks without an 'overlay' field don't have linkBlocks + * I have no clue what they are and why there even exist + * + if(!contents.containsKey("overlay")){ + println(contents) + continue + TODO check and correct + }*/ + + val result = contents.jsonObject["musicResponsiveListItemRenderer"] + ?.jsonObject?.get("flexColumns")?.jsonArray + + // Add the linkBlock + val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"] + ?.jsonObject?.get("overlay") + ?.jsonObject?.get("musicItemThumbnailOverlayRenderer") + ?.jsonObject?.get("content") + ?.jsonObject?.get("musicPlayButtonRenderer") + ?.jsonObject?.get("playNavigationEndpoint") + + // detailsBlock is always a list, so we just append the linkBlock to it + // instead of carrying along all the other junk from "musicResponsiveListItemRenderer" + val finalResult = buildJsonArray { + result?.let { add(it) } + linkBlock?.let { add(it) } + } + resultBlocks.add(finalResult) } - resultBlocks.add(finalResult) } - } - /* We only need results that are Songs or Videos, so we filter out the rest, since - ! Songs and Videos are supplied with different details, extracting all details from - ! both is just carrying on redundant data, so we also have to selectively extract - ! relevant details. What you need to know to understand how we do that here: - ! - ! Songs details are ALWAYS in the following order: - ! 0 - Name - ! 1 - Type (Song) - ! 2 - com.shabinder.spotiflyer.models.gaana.Artist - ! 3 - Album - ! 4 - Duration (mm:ss) - ! - ! Video details are ALWAYS in the following order: - ! 0 - Name - ! 1 - Type (Video) - ! 2 - Channel - ! 3 - Viewers - ! 4 - Duration (hh:mm:ss) - ! - ! We blindly gather all the details we get our hands on, then - ! cherry pick the details we need based on their index numbers, - ! we do so only if their Type is 'Song' or 'Video - */ - - for (result in resultBlocks) { - - // Blindly gather available details - val availableDetails = mutableListOf() - - /* - Filter Out dummies here itself - ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one - ! sub-block, if not its a dummy, why does the YTM response contain dummies? - ! I have no clue. We skip these. - - ! Remember that we appended the linkBlock to result, treating that like the - ! other constituents of a result block will lead to errors, hence the 'in - ! result[:-1] ,i.e., skip last element in array ' + /* We only need results that are Songs or Videos, so we filter out the rest, since + ! Songs and Videos are supplied with different details, extracting all details from + ! both is just carrying on redundant data, so we also have to selectively extract + ! relevant details. What you need to know to understand how we do that here: + ! + ! Songs details are ALWAYS in the following order: + ! 0 - Name + ! 1 - Type (Song) + ! 2 - com.shabinder.spotiflyer.models.gaana.Artist + ! 3 - Album + ! 4 - Duration (mm:ss) + ! + ! Video details are ALWAYS in the following order: + ! 0 - Name + ! 1 - Type (Video) + ! 2 - Channel + ! 3 - Viewers + ! 4 - Duration (hh:mm:ss) + ! + ! We blindly gather all the details we get our hands on, then + ! cherry pick the details we need based on their index numbers, + ! we do so only if their Type is 'Song' or 'Video */ - for (detailArray in result.subList(0, result.size - 1)) { - for (detail in detailArray.jsonArray) { - if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue - // if not a dummy, collect All Variables - val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"] - ?.jsonObject?.get("text") - ?.jsonObject?.get("runs")?.jsonArray ?: listOf() + for (result in resultBlocks) { - for (d in details) { - d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let { - if (it != " • ") { - availableDetails.add(it) + // Blindly gather available details + val availableDetails = mutableListOf() + + /* + Filter Out dummies here itself + ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one + ! sub-block, if not its a dummy, why does the YTM response contain dummies? + ! I have no clue. We skip these. + + ! Remember that we appended the linkBlock to result, treating that like the + ! other constituents of a result block will lead to errors, hence the 'in + ! result[:-1] ,i.e., skip last element in array ' + */ + for (detailArray in result.subList(0, result.size - 1)) { + for (detail in detailArray.jsonArray) { + if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue + + // if not a dummy, collect All Variables + val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"] + ?.jsonObject?.get("text") + ?.jsonObject?.get("runs")?.jsonArray ?: listOf() + + for (d in details) { + d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let { + if (it != " • ") { + availableDetails.add(it) + } } } } } - } - // logger.d("YT Music details"){availableDetails.toString()} - /* - ! Filter Out non-Song/Video results and incomplete results here itself - ! From what we know about detail order, note that [1] - indicate result type - */ - if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) { - - // skip if result is in hours instead of minutes (no song is that long) - if (availableDetails[4].split(':').size != 2) continue - + // logger.d("YT Music details"){availableDetails.toString()} /* - ! grab Video ID - ! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...] - ! so hardcoding the dict keys for data look up is an ardours process, since - ! the sub-block pattern is fixed even though the key isn't, we just - ! reference the dict keys by index + ! Filter Out non-Song/Video results and incomplete results here itself + ! From what we know about detail order, note that [1] - indicate result type */ + if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) { - val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content - val ytTrack = YoutubeTrack( - name = availableDetails[0], - type = availableDetails[1], - artist = availableDetails[2], - duration = availableDetails[4], - videoId = videoId - ) - youtubeTracks.add(ytTrack) + // skip if result is in hours instead of minutes (no song is that long) + if (availableDetails[4].split(':').size != 2) continue + + /* + ! grab Video ID + ! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...] + ! so hardcoding the dict keys for data look up is an ardours process, since + ! the sub-block pattern is fixed even though the key isn't, we just + ! reference the dict keys by index + */ + + val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content + val ytTrack = YoutubeTrack( + name = availableDetails[0], + type = availableDetails[1], + artist = availableDetails[2], + duration = availableDetails[4], + videoId = videoId + ) + youtubeTracks.add(ytTrack) + } } } - } // logger.d {youtubeTracks.joinToString("\n")} - return youtubeTracks + youtubeTracks } private fun sortByBestMatch( @@ -246,8 +249,8 @@ class YoutubeMusic constructor( // most song results on youtube go by $artist - $songName or artist1/artist2 var hasCommonWord = false - val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: "" - val trackNameWords = trackName.toLowerCase().split(" ") + val resultName = result.name?.lowercase()?.replace("-", " ")?.replace("/", " ") ?: "" + val trackNameWords = trackName.lowercase().split(" ") for (nameWord in trackNameWords) { if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true @@ -266,12 +269,12 @@ class YoutubeMusic constructor( if (result.type == "Song") { for (artist in trackArtists) { - if (FuzzySearch.ratio(artist.toLowerCase(), result.artist?.toLowerCase() ?: "") > 85) + if (FuzzySearch.ratio(artist.lowercase(), result.artist?.lowercase() ?: "") > 85) artistMatchNumber++ } } else { // i.e. is a Video for (artist in trackArtists) { - if (FuzzySearch.partialRatio(artist.toLowerCase(), result.name?.toLowerCase() ?: "") > 85) + if (FuzzySearch.partialRatio(artist.lowercase(), result.name?.lowercase() ?: "") > 85) artistMatchNumber++ } } @@ -303,9 +306,8 @@ class YoutubeMusic constructor( } } - private suspend fun getYoutubeMusicResponse(query: String): String { - logger.i { "Fetching Youtube Music Response" } - return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") { + private suspend fun getYoutubeMusicResponse(query: String): SuspendableEvent = SuspendableEvent { + httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") { contentType(ContentType.Application.Json) headers { append("referer", "https://music.youtube.com/search") diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt index 6f720ac8..8ae28336 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt @@ -22,7 +22,9 @@ import com.shabinder.common.di.finalOutputDir import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.spotify.Source import io.github.shabinder.YoutubeDownloader import io.github.shabinder.models.YoutubeVideo @@ -49,7 +51,7 @@ class YoutubeProvider( private val sampleDomain2 = "youtube.com" private val sampleDomain3 = "youtu.be" - suspend fun query(fullLink: String): PlatformQueryResult? { + suspend fun query(fullLink: String): SuspendableEvent { val link = fullLink.removePrefix("https://").removePrefix("http://") if (link.contains("playlist", true) || link.contains("list", true)) { // Given Link is of a Playlist @@ -77,74 +79,15 @@ class YoutubeProvider( ) } else { logger.d { "Your Youtube Link is not of a Video!!" } - null + SuspendableEvent.error(SpotiFlyerException.LinkInvalid(fullLink)) } } } private suspend fun getYTPlaylist( searchId: String - ): PlatformQueryResult? { - val result = PlatformQueryResult( - folderType = "", - subFolder = "", - title = "", - coverUrl = "", - trackList = listOf(), - Source.YouTube - ) - result.apply { - try { - val playlist = ytDownloader.getPlaylist(searchId) - val playlistDetails = playlist.details - val name = playlistDetails.title - subFolder = removeIllegalChars(name) - val videos = playlist.videos - - coverUrl = "https://i.ytimg.com/vi/${ - videos.firstOrNull()?.videoId - }/hqdefault.jpg" - title = name - - trackList = videos.map { - TrackDetails( - title = it.title ?: "N/A", - artists = listOf(it.author ?: "N/A"), - durationSec = it.lengthSeconds, - albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg", - source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg", - downloaded = if (dir.isPresent( - dir.finalOutputDir( - itemName = it.title ?: "N/A", - type = folderType, - subFolder = subFolder, - dir.defaultDir() - ) - ) - ) - DownloadStatus.Downloaded - else { - DownloadStatus.NotDownloaded - }, - outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/), - videoID = it.videoId - ) - } - } catch (e: Exception) { - e.printStackTrace() - logger.d { "An Error Occurred While Processing!" } - } - } - return if (result.title.isNotBlank()) result - else null - } - - @Suppress("DefaultLocale") - private suspend fun getYTTrack( - searchId: String, - ): PlatformQueryResult? { - val result = PlatformQueryResult( + ): SuspendableEvent = SuspendableEvent { + PlatformQueryResult( folderType = "", subFolder = "", title = "", @@ -152,47 +95,90 @@ class YoutubeProvider( trackList = listOf(), Source.YouTube ).apply { - try { - logger.i { searchId } - val video = ytDownloader.getVideo(searchId) - coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" - val detail = video.videoDetails - val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true) - ?: detail.title ?: "" - // logger.i{ detail.toString() } - trackList = listOf( - TrackDetails( - title = name, - artists = listOf(detail.author ?: "N/A"), - durationSec = detail.lengthSeconds, - albumArtPath = dir.imageCacheDir() + "$searchId.jpeg", - source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", - downloaded = if (dir.isPresent( - dir.finalOutputDir( - itemName = name, - type = folderType, - subFolder = subFolder, - defaultDir = dir.defaultDir() - ) + val playlist = ytDownloader.getPlaylist(searchId) + val playlistDetails = playlist.details + val name = playlistDetails.title + subFolder = removeIllegalChars(name) + val videos = playlist.videos + + coverUrl = "https://i.ytimg.com/vi/${ + videos.firstOrNull()?.videoId + }/hqdefault.jpg" + title = name + + trackList = videos.map { + TrackDetails( + title = it.title ?: "N/A", + artists = listOf(it.author ?: "N/A"), + durationSec = it.lengthSeconds, + albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg", + source = Source.YouTube, + albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg", + downloaded = if (dir.isPresent( + dir.finalOutputDir( + itemName = it.title ?: "N/A", + type = folderType, + subFolder = subFolder, + dir.defaultDir() ) ) - DownloadStatus.Downloaded - else { - DownloadStatus.NotDownloaded - }, - outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/), - videoID = searchId ) + DownloadStatus.Downloaded + else { + DownloadStatus.NotDownloaded + }, + outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/), + videoID = it.videoId ) - title = name - } catch (e: Exception) { - e.printStackTrace() - logger.e { "An Error Occurred While Processing!,$searchId" } } } - return if (result.title.isNotBlank()) result - else null + } + + @Suppress("DefaultLocale") + private suspend fun getYTTrack( + searchId: String, + ): SuspendableEvent = SuspendableEvent { + PlatformQueryResult( + folderType = "", + subFolder = "", + title = "", + coverUrl = "", + trackList = listOf(), + Source.YouTube + ).apply { + val video = ytDownloader.getVideo(searchId) + coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" + val detail = video.videoDetails + val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true) + ?: detail.title ?: "" + // logger.i{ detail.toString() } + trackList = listOf( + TrackDetails( + title = name, + artists = listOf(detail.author ?: "N/A"), + durationSec = detail.lengthSeconds, + albumArtPath = dir.imageCacheDir() + "$searchId.jpeg", + source = Source.YouTube, + albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", + downloaded = if (dir.isPresent( + dir.finalOutputDir( + itemName = name, + type = folderType, + subFolder = subFolder, + defaultDir = dir.defaultDir() + ) + ) + ) + DownloadStatus.Downloaded + else { + DownloadStatus.NotDownloaded + }, + outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/), + videoID = searchId + ) + ) + title = name + } } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt index 3c6ceb7c..b3b94675 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt @@ -2,8 +2,8 @@ package com.shabinder.common.di.saavn import co.touchlab.kermit.Kermit import com.shabinder.common.di.audioToMp3.AudioToMp3 -import com.shabinder.common.di.gaana.corsApi import com.shabinder.common.di.globalJson +import com.shabinder.common.models.corsApi import com.shabinder.common.models.saavn.SaavnAlbum import com.shabinder.common.models.saavn.SaavnPlaylist import com.shabinder.common.models.saavn.SaavnSearchResult @@ -13,9 +13,9 @@ import io.github.shabinder.utils.getBoolean import io.github.shabinder.utils.getJsonArray import io.github.shabinder.utils.getJsonObject import io.github.shabinder.utils.getString -import io.ktor.client.HttpClient -import io.ktor.client.features.ServerResponseException -import io.ktor.client.request.get +import io.ktor.client.* +import io.ktor.client.features.* +import io.ktor.client.request.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -24,6 +24,7 @@ import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put +import kotlin.collections.set interface JioSaavnRequests { @@ -237,8 +238,8 @@ interface JioSaavnRequests { for (result in tracks) { var hasCommonWord = false - val resultName = result.title.toLowerCase().replace("/", " ") - val trackNameWords = trackName.toLowerCase().split(" ") + val resultName = result.title.lowercase().replace("/", " ") + val trackNameWords = trackName.lowercase().split(" ") for (nameWord in trackNameWords) { if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true @@ -258,11 +259,11 @@ interface JioSaavnRequests { // String Containing All Artist Names from JioSaavn Search Result val artistListString = mutableSetOf().apply { result.more_info?.singers?.split(",")?.let { addAll(it) } - result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) } + result.more_info?.primary_artists?.lowercase()?.split(",")?.let { addAll(it) } }.joinToString(" , ") for (artist in trackArtists) { - if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85) + if (FuzzySearch.partialRatio(artist.lowercase(), artistListString) > 85) artistMatchNumber++ } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt index 363eaf17..0bd18414 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt @@ -19,14 +19,14 @@ package com.shabinder.common.di.spotify import com.shabinder.common.di.globalJson import com.shabinder.common.models.methods import com.shabinder.common.models.spotify.TokenData -import io.ktor.client.HttpClient -import io.ktor.client.features.auth.Auth -import io.ktor.client.features.auth.providers.basic -import io.ktor.client.features.json.JsonFeature -import io.ktor.client.features.json.serializer.KotlinxSerializer -import io.ktor.client.request.forms.FormDataContent -import io.ktor.client.request.post -import io.ktor.http.Parameters +import io.ktor.client.* +import io.ktor.client.features.auth.* +import io.ktor.client.features.auth.providers.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* import kotlin.native.concurrent.SharedImmutable suspend fun authenticateSpotify(): TokenData? { @@ -48,9 +48,10 @@ private val spotifyAuthClient by lazy { install(Auth) { basic { - sendWithoutRequest = true - username = clientId - password = clientSecret + sendWithoutRequest { true } + credentials { + BasicAuthCredentials(clientId, clientSecret) + } } } install(JsonFeature) { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt index dba7784e..22acff05 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt @@ -16,14 +16,16 @@ package com.shabinder.common.di.spotify -import com.shabinder.common.di.gaana.corsApi import com.shabinder.common.models.NativeAtomicReference +import com.shabinder.common.models.corsApi import com.shabinder.common.models.spotify.Album import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack import com.shabinder.common.models.spotify.Playlist import com.shabinder.common.models.spotify.Track -import io.ktor.client.HttpClient -import io.ktor.client.request.get +import io.github.shabinder.TargetPlatforms +import io.github.shabinder.activePlatform +import io.ktor.client.* +import io.ktor.client.request.* private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1" @@ -32,7 +34,7 @@ interface SpotifyRequests { val httpClientRef: NativeAtomicReference val httpClient: HttpClient get() = httpClientRef.value - suspend fun authenticateSpotifyClient(override: Boolean = false) + suspend fun authenticateSpotifyClient(override: Boolean = activePlatform is TargetPlatforms.Js) suspend fun getPlaylist(playlistID: String): Playlist { return httpClient.get("$BASE_URL/playlists/$playlistID") diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt index 4a1613a5..f965c6bc 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt @@ -17,11 +17,13 @@ package com.shabinder.common.di.youtubeMp3 import co.touchlab.kermit.Kermit -import com.shabinder.common.di.gaana.corsApi -import io.ktor.client.HttpClient -import io.ktor.client.request.forms.FormDataContent -import io.ktor.client.request.post -import io.ktor.http.Parameters +import com.shabinder.common.models.corsApi +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.requireNotNull +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonPrimitive @@ -33,18 +35,24 @@ interface Yt1sMp3 { val httpClient: HttpClient val logger: Kermit + /* * Downloadable Mp3 Link for YT videoID. * */ - suspend fun getLinkFromYt1sMp3(videoID: String): String? = - getConvertedMp3Link(videoID, getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "") + suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent = SuspendableEvent { + getConvertedMp3Link( + videoID, + getKey(videoID).value + ).value["dlink"].requireNotNull() + .jsonPrimitive.content.replace("\"", "") + } /* * POST:https://yt1s.com/api/ajaxSearch/index * Body Form= q:yt video link ,vt:format=mp3 * */ - private suspend fun getKey(videoID: String): String { - val response: JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") { + private suspend fun getKey(videoID: String): SuspendableEvent = SuspendableEvent { + val response: JsonObject = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") { body = FormDataContent( Parameters.build { append("q", "https://www.youtube.com/watch?v=$videoID") @@ -52,11 +60,12 @@ interface Yt1sMp3 { } ) } - return response?.get("kc")?.jsonPrimitive.toString() + + response["kc"].requireNotNull().jsonPrimitive.content } - private suspend fun getConvertedMp3Link(videoID: String, key: String): JsonObject? { - return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") { + private suspend fun getConvertedMp3Link(videoID: String, key: String): SuspendableEvent = SuspendableEvent { + httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") { body = FormDataContent( Parameters.build { append("vid", videoID) diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index 6bf00c69..cbc449d7 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -17,7 +17,6 @@ package com.shabinder.common.di import com.shabinder.common.di.utils.ParallelExecutor -import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails @@ -34,9 +33,6 @@ val DownloadScope = ParallelExecutor(Dispatchers.IO) // IO-Dispatcher actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO -// Current Platform Info -actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm - actual suspend fun downloadTracks( list: List, fetcher: FetchPlatformQueryResult, diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt index 7a01defa..5c0740a1 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt @@ -16,7 +16,6 @@ package com.shabinder.common.di -import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails @@ -34,9 +33,6 @@ val allTracksStatus: HashMap = hashMapOf() // IO-Dispatcher actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default -// Current Platform Info -actual val currentPlatform: AllPlatforms = AllPlatforms.Js - actual suspend fun downloadTracks( list: List, fetcher: FetchPlatformQueryResult, diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index a0921a43..d322659a 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -28,12 +28,10 @@ import com.arkivanov.decompose.statekeeper.Parcelable import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.value.Value import com.shabinder.common.di.Dir -import com.shabinder.common.di.currentPlatform import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.models.Actions -import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.Consumer import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot @@ -80,10 +78,7 @@ internal class SpotiFlyerRootImpl( instanceKeeper.ensureNeverFrozen() methods.value = dependencies.actions.freeze() /*Authenticate Spotify Client*/ - authenticateSpotify( - dependencies.fetchPlatformQueryResult.spotifyProvider, - currentPlatform is AllPlatforms.Js - ) + authenticateSpotify(dependencies.fetchPlatformQueryResult.spotifyProvider) } private val router = @@ -134,11 +129,11 @@ internal class SpotiFlyerRootImpl( } } - private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override: Boolean) { + private fun authenticateSpotify(spotifyProvider: SpotifyProvider) { GlobalScope.launch(Dispatchers.Default) { analytics.appLaunchEvent() /*Authenticate Spotify Client*/ - spotifyProvider.authenticateSpotifyClient(override) + spotifyProvider.authenticateSpotifyClient() } } From 8d5e9cdcccc883d425c5933edca9136784c9ec5e Mon Sep 17 00:00:00 2001 From: shabinder Date: Mon, 21 Jun 2021 17:55:35 +0530 Subject: [PATCH 03/15] Error Handling WIP --- .../common/models/SpotiFlyerException.kt | 10 ++ .../shabinder/common/models/event/Event.kt | 19 ++-- .../event/coroutines/SuspendableEvent.kt | 37 ++++-- .../common/di/worker/ForegroundService.kt | 21 ++-- .../common/di/FetchPlatformQueryResult.kt | 72 ++++++++---- .../com/shabinder/common/di/TokenStore.kt | 2 +- .../common/di/audioToMp3/AudioToMp3.kt | 41 ++++--- .../common/di/providers/SaavnProvider.kt | 6 +- .../common/di/providers/SpotifyProvider.kt | 6 +- .../common/di/providers/YoutubeMusic.kt | 23 ++-- .../common/di/saavn/JioSaavnRequests.kt | 106 +++++++++--------- .../common/di/spotify/SpotifyAuth.kt | 15 ++- .../shabinder/common/di/youtubeMp3/Yt1sMp3.kt | 13 ++- .../shabinder/common/list/SpotiFlyerList.kt | 2 +- .../list/store/SpotiFlyerListStoreProvider.kt | 18 ++- .../root/integration/SpotiFlyerRootImpl.kt | 13 +-- settings.gradle.kts | 8 ++ 17 files changed, 235 insertions(+), 177 deletions(-) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index 472724e2..65b31c1d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -3,6 +3,7 @@ package com.shabinder.common.models sealed class SpotiFlyerException(override val message: String): Exception(message) { data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) + data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message) data class NoMatchFound( val trackName: String? = null, @@ -14,6 +15,15 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag override val message: String = "No Downloadable link found for videoID: $videoID" ): SpotiFlyerException(message) + data class DownloadLinkFetchFailed( + val trackName: String, + val jioSaavnError: Throwable, + val ytMusicError: Throwable, + override val message: String = "No Downloadable link found for track: $trackName," + + " \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n " + + " \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " + ): SpotiFlyerException(message) + data class LinkInvalid( val link: String? = null, override val message: String = "Entered Link is NOT Valid!\n ${link ?: ""}" diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt index e3d6621d..76cfb021 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt @@ -1,5 +1,8 @@ package com.shabinder.common.models.event +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + inline fun Event<*, *>.getAs() = when (this) { is Event.Success -> value as? X is Event.Failure -> error as? X @@ -128,7 +131,7 @@ inline fun Event.unwrapError(success: (V) -> Nothing): apply { component1()?.let(success) }.component2()!! -sealed class Event { +sealed class Event: ReadOnlyProperty { open operator fun component1(): V? = null open operator fun component2(): E? = null @@ -138,13 +141,11 @@ sealed class Event { is Failure -> failure(this.error) } - abstract fun get(): V + abstract val value: V - class Success(val value: V) : Event() { + class Success(override val value: V) : Event() { override fun component1(): V? = value - override fun get(): V = value - override fun toString() = "[Success: $value]" override fun hashCode(): Int = value.hashCode() @@ -153,12 +154,14 @@ sealed class Event { if (this === other) return true return other is Success<*> && value == other.value } + + override fun getValue(thisRef: Any?, property: KProperty<*>): V = value } class Failure(val error: E) : Event() { - override fun component2(): E? = error + override fun component2(): E = error - override fun get() = throw error + override val value: Nothing = throw error fun getThrowable(): E = error @@ -170,6 +173,8 @@ sealed class Event { if (this === other) return true return other is Failure<*> && error == other.error } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Nothing = value } companion object { diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt index 9e739ef8..6e474800 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt @@ -1,5 +1,8 @@ package com.shabinder.common.models.event.coroutines +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + inline fun SuspendableEvent<*, *>.getAs() = when (this) { is SuspendableEvent.Success -> value as? X is SuspendableEvent.Failure -> error as? X @@ -52,16 +55,24 @@ suspend inline fun SuspendableEvent.fl suspend inline fun SuspendableEvent.mapError( crossinline transform: suspend (E) -> E2 -) = when (this) { - is SuspendableEvent.Success -> SuspendableEvent.Success(value) - is SuspendableEvent.Failure -> SuspendableEvent.Failure(transform(error)) +) = try { + when (this) { + is SuspendableEvent.Success -> SuspendableEvent.Success(value) + is SuspendableEvent.Failure -> SuspendableEvent.Failure(transform(error)) + } +} catch (ex: Throwable) { + SuspendableEvent.error(ex as E) } suspend inline fun SuspendableEvent.flatMapError( crossinline transform: suspend (E) -> SuspendableEvent -) = when (this) { - is SuspendableEvent.Success -> SuspendableEvent.Success(value) - is SuspendableEvent.Failure -> transform(error) +) = try { + when (this) { + is SuspendableEvent.Success -> SuspendableEvent.Success(value) + is SuspendableEvent.Failure -> transform(error) + } +} catch (ex: Throwable) { + SuspendableEvent.error(ex as E) } suspend inline fun SuspendableEvent.any( @@ -89,7 +100,7 @@ suspend fun List>.lift(): Suspe } } -sealed class SuspendableEvent { +sealed class SuspendableEvent: ReadOnlyProperty { abstract operator fun component1(): V? abstract operator fun component2(): E? @@ -115,6 +126,8 @@ sealed class SuspendableEvent { if (this === other) return true return other is Success<*, *> && value == other.value } + + override fun getValue(thisRef: Any?, property: KProperty<*>): V = value } class Failure(val error: E) : SuspendableEvent() { @@ -133,6 +146,8 @@ sealed class SuspendableEvent { if (this === other) return true return other is Failure<*, *> && error == other.error } + + override fun getValue(thisRef: Any?, property: KProperty<*>): V = value } companion object { @@ -143,14 +158,14 @@ sealed class SuspendableEvent { return value?.let { Success(it) } ?: error(fail()) } - suspend inline fun of(crossinline f: suspend () -> V): SuspendableEvent = try { - Success(f()) + suspend inline fun of(crossinline block: suspend () -> V): SuspendableEvent = try { + Success(block()) } catch (ex: Throwable) { Failure(ex as E) } - suspend inline operator fun invoke(crossinline f: suspend () -> V): SuspendableEvent = try { - Success(f()) + suspend inline operator fun invoke(crossinline block: suspend () -> V): SuspendableEvent = try { + Success(block()) } catch (ex: Throwable) { Failure(ex) } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt index cbf3079d..f2bf595c 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt @@ -76,7 +76,6 @@ class ForegroundService : Service(), CoroutineScope { private lateinit var downloadManager: DownloadManager private lateinit var downloadService: ParallelExecutor - private val ytDownloader get() = fetcher.youtubeProvider.ytDownloader private val fetcher: FetchPlatformQueryResult by inject() private val logger: Kermit by inject() private val dir: Dir by inject() @@ -161,15 +160,17 @@ class ForegroundService : Service(), CoroutineScope { trackList.forEach { launch(Dispatchers.IO) { downloadService.execute { - val url = fetcher.findMp3DownloadLink(it) - if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL - enqueueDownload(url, it) - } else { - sendTrackBroadcast(Status.FAILED.name, it) - failed++ - updateNotification() - allTracksStatus[it.title] = DownloadStatus.Failed - } + fetcher.findMp3DownloadLink(it).fold( + success = { url -> + enqueueDownload(url, it) + }, + failure = { _ -> + sendTrackBroadcast(Status.FAILED.name, it) + failed++ + updateNotification() + allTracksStatus[it.title] = DownloadStatus.Failed + } + ) } } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt index 58a842d9..2e7bb311 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt @@ -26,25 +26,33 @@ import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeProvider import com.shabinder.common.di.providers.get import com.shabinder.common.models.PlatformQueryResult +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.flatMap +import com.shabinder.common.models.event.coroutines.flatMapError +import com.shabinder.common.models.event.coroutines.success import com.shabinder.common.models.spotify.Source +import com.shabinder.common.requireNotNull import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class FetchPlatformQueryResult( private val gaanaProvider: GaanaProvider, - val spotifyProvider: SpotifyProvider, - val youtubeProvider: YoutubeProvider, + private val spotifyProvider: SpotifyProvider, + private val youtubeProvider: YoutubeProvider, private val saavnProvider: SaavnProvider, - val youtubeMusic: YoutubeMusic, - val youtubeMp3: YoutubeMp3, - val audioToMp3: AudioToMp3, + private val youtubeMusic: YoutubeMusic, + private val youtubeMp3: YoutubeMp3, + private val audioToMp3: AudioToMp3, val dir: Dir ) { private val db: DownloadRecordDatabaseQueries? get() = dir.db?.downloadRecordDatabaseQueries - suspend fun query(link: String): PlatformQueryResult? { + suspend fun authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient() + + suspend fun query(link: String): SuspendableEvent { val result = when { // SPOTIFY link.contains("spotify", true) -> @@ -63,13 +71,13 @@ class FetchPlatformQueryResult( gaanaProvider.query(link) else -> { - null + SuspendableEvent.error(SpotiFlyerException.LinkInvalid(link)) } } - if (result != null) { + result.success { addToDatabaseAsync( link, - result.copy() // Send a copy in order to not to freeze Result itself + it.copy() // Send a copy in order to not to freeze Result itself ) } return result @@ -79,35 +87,53 @@ class FetchPlatformQueryResult( // 2) If Not found try finding on Youtube Music suspend fun findMp3DownloadLink( track: TrackDetails - ): String? = + ): SuspendableEvent = if (track.videoID != null) { // We Already have VideoID when (track.source) { Source.JioSaavn -> { - saavnProvider.getSongFromID(track.videoID!!).media_url?.let { m4aLink -> - audioToMp3.convertToMp3(m4aLink) + saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song -> + song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findHighestQualityMp3Link(track) } } Source.YouTube -> { - youtubeMp3.getMp3DownloadLink(track.videoID!!) - ?: youtubeProvider.ytDownloader?.getVideo(track.videoID!!)?.get()?.url?.let { m4aLink -> + youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull()).flatMapError { + youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink -> audioToMp3.convertToMp3(m4aLink) - } + } ?: throw SpotiFlyerException.YoutubeLinkNotFound(track.videoID) + } } else -> { - null/* Do Nothing, We should never reach here for now*/ + /*We should never reach here for now*/ + findHighestQualityMp3Link(track) } } } else { - // First Try Getting A Link From JioSaavn - saavnProvider.findSongDownloadURL( - trackName = track.title, - trackArtists = track.artists - ) - // Lets Try Fetching Now From Youtube Music - ?: youtubeMusic.findSongDownloadURL(track) + findHighestQualityMp3Link(track) } + private suspend fun findHighestQualityMp3Link( + track: TrackDetails + ):SuspendableEvent { + // Try Fetching Track from Jio Saavn + return saavnProvider.findMp3SongDownloadURL( + trackName = track.title, + trackArtists = track.artists + ).flatMapError { saavnError -> + // Lets Try Fetching Now From Youtube Music + youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError -> + // If Both Failed Bubble the Exception Up with both StackTraces + SuspendableEvent.error( + SpotiFlyerException.DownloadLinkFetchFailed( + trackName = track.title, + jioSaavnError = saavnError, + ytMusicError = ytMusicError + ) + ) + } + } + } + private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) { GlobalScope.launch(dispatcherIO) { db?.add( diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt index 0d139628..041fb9a3 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt @@ -43,7 +43,7 @@ class TokenStore( logger.d { "System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}" } if (Clock.System.now().epochSeconds > token?.expiry ?: 0 || token == null) { logger.d { "Requesting New Token" } - token = authenticateSpotify() + token = authenticateSpotify().component1() GlobalScope.launch { token?.access_token?.let { save(token) } } } return token diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt index f40d62b6..0a81e57a 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt @@ -2,14 +2,12 @@ package com.shabinder.common.di.audioToMp3 import co.touchlab.kermit.Kermit import com.shabinder.common.models.AudioQuality -import io.ktor.client.HttpClient -import io.ktor.client.request.forms.formData -import io.ktor.client.request.forms.submitFormWithBinaryData -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.request.headers -import io.ktor.client.statement.HttpStatement -import io.ktor.http.isSuccess +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* import kotlinx.coroutines.delay interface AudioToMp3 { @@ -32,9 +30,9 @@ interface AudioToMp3 { suspend fun convertToMp3( URL: String, audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)), - ): String? { - val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send - val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd + ): SuspendableEvent = SuspendableEvent { + val activeHost by getHost() // ex - https://hostveryfast.onlineconverter.com/file/send + val jobLink by convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd // (jobStatus.contains("d")) == COMPLETION var jobStatus: String @@ -54,10 +52,7 @@ interface AudioToMp3 { if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio } while (!jobStatus.contains("d", true) && retryCount != 0) - return if (jobStatus.equals("d", true)) { - // Return MP3 Download Link - "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download" - } else null + "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download" } /* @@ -68,8 +63,8 @@ interface AudioToMp3 { URL: String, host: String? = null, audioQuality: AudioQuality = AudioQuality.KBPS160, - ): String { - val activeHost = host ?: getHost() + ): SuspendableEvent = SuspendableEvent { + val activeHost = host ?: getHost().value val res = client.submitFormWithBinaryData( url = activeHost, formData = formData { @@ -87,7 +82,7 @@ interface AudioToMp3 { header("Referer", "https://www.onlineconverter.com/") } }.run { - logger.d { this } + // logger.d { this } dropLast(3) // last 3 are useless unicode char } @@ -97,18 +92,20 @@ interface AudioToMp3 { } }.execute() logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() } - return res + + res } // Active Host free to process conversion // ex - https://hostveryfast.onlineconverter.com/file/send - private suspend fun getHost(): String { - return client.get("https://www.onlineconverter.com/get/host") { + private suspend fun getHost(): SuspendableEvent = SuspendableEvent { + client.get("https://www.onlineconverter.com/get/host") { headers { header("Host", "www.onlineconverter.com") } - }.also { logger.i("Active Host") { it } } + }//.also { logger.i("Active Host") { it } } } + // Extract full Domain from URL // ex - hostveryfast.onlineconverter.com private fun String.getHostDomain(): String { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt index ca78e490..b062f4cf 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt @@ -33,7 +33,7 @@ class SaavnProvider( ).apply { when (fullLink.substringAfter("saavn.com/").substringBefore("/")) { "song" -> { - getSong(fullLink).let { + getSong(fullLink).value.let { folderType = "Tracks" subFolder = "" trackList = listOf(it).toTrackDetails(folderType, subFolder) @@ -42,7 +42,7 @@ class SaavnProvider( } } "album" -> { - getAlbum(fullLink)?.let { + getAlbum(fullLink).value.let { folderType = "Albums" subFolder = removeIllegalChars(it.title) trackList = it.songs.toTrackDetails(folderType, subFolder) @@ -51,7 +51,7 @@ class SaavnProvider( } } "featured" -> { // Playlist - getPlaylist(fullLink)?.let { + getPlaylist(fullLink).value.let { folderType = "Playlists" subFolder = removeIllegalChars(it.listname) trackList = it.songs.toTrackDetails(folderType, subFolder) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index ac85927c..b6796258 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -48,9 +48,9 @@ class SpotifyProvider( ) : SpotifyRequests { override suspend fun authenticateSpotifyClient(override: Boolean) { - val token = if (override) authenticateSpotify() else tokenStore.getToken() + val token = if (override) authenticateSpotify().component1() else tokenStore.getToken() if (token == null) { - logger.d { "Please Check your Network Connection" } + logger.d { "Spotify Auth Failed: Please Check your Network Connection" } } else { logger.d { "Spotify Provider Created with $token" } HttpClient { @@ -183,8 +183,10 @@ class SpotifyProvider( coverUrl = playlistObject.images?.firstOrNull()?.url.toString() } "episode" -> { // TODO + throw SpotiFlyerException.FeatureNotImplementedYet() } "show" -> { // TODO + throw SpotiFlyerException.FeatureNotImplementedYet() } else -> { throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link") diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index 195f615f..ec68f6bc 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -46,28 +46,29 @@ import kotlin.math.absoluteValue class YoutubeMusic constructor( private val logger: Kermit, private val httpClient: HttpClient, - private val youtubeMp3: YoutubeMp3, private val youtubeProvider: YoutubeProvider, + private val youtubeMp3: YoutubeMp3, private val audioToMp3: AudioToMp3 ) { - companion object { const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30" const val tag = "YT Music" } - suspend fun findSongDownloadURL( + // Get Downloadable Link + suspend fun findMp3SongDownloadURLYT( trackDetails: TrackDetails ): SuspendableEvent { - val bestMatchVideoID = getYTIDBestMatch(trackDetails) - return bestMatchVideoID.flatMap { videoID -> - // Get Downloadable Link + return getYTIDBestMatch(trackDetails).flatMap { videoID -> + // 1 Try getting Link from Yt1s youtubeMp3.getMp3DownloadLink(videoID).flatMapError { - SuspendableEvent { - youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink -> - audioToMp3.convertToMp3(m4aLink) - } ?: throw SpotiFlyerException.YoutubeLinkNotFound(videoID) - } + // 2 if Yt1s failed , Extract Manually + youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink -> + audioToMp3.convertToMp3(m4aLink) + } ?: throw SpotiFlyerException.YoutubeLinkNotFound( + videoID, + message = "Caught Following Errors While Finding Downloadable Link for $videoID : \n${it.stackTraceToString()}" + ) } } } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt index b3b94675..79c6188c 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt @@ -4,17 +4,20 @@ import co.touchlab.kermit.Kermit import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.globalJson import com.shabinder.common.models.corsApi +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.map +import com.shabinder.common.models.event.coroutines.success import com.shabinder.common.models.saavn.SaavnAlbum import com.shabinder.common.models.saavn.SaavnPlaylist import com.shabinder.common.models.saavn.SaavnSearchResult import com.shabinder.common.models.saavn.SaavnSong +import com.shabinder.common.requireNotNull import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch import io.github.shabinder.utils.getBoolean import io.github.shabinder.utils.getJsonArray import io.github.shabinder.utils.getJsonObject import io.github.shabinder.utils.getString import io.ktor.client.* -import io.ktor.client.features.* import io.ktor.client.request.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -32,63 +35,64 @@ interface JioSaavnRequests { val httpClient: HttpClient val logger: Kermit - suspend fun findSongDownloadURL( + suspend fun findMp3SongDownloadURL( trackName: String, trackArtists: List, - ): String? { - val songs = searchForSong(trackName) + ): SuspendableEvent = searchForSong(trackName).map { songs -> val bestMatches = sortByBestMatch(songs, trackName, trackArtists) - val m4aLink: String? = bestMatches.keys.firstOrNull()?.let { - getSongFromID(it).media_url + + val m4aLink: String by getSongFromID(bestMatches.keys.first()).map { song -> + song.media_url.requireNotNull() } - val mp3Link = m4aLink?.let { audioToMp3.convertToMp3(it) } - return mp3Link + + val mp3Link by audioToMp3.convertToMp3(m4aLink) + + mp3Link } suspend fun searchForSong( query: String, includeLyrics: Boolean = false - ): List { - /*if (query.startsWith("http") && query.contains("saavn.com")) { - return listOf(getSong(query)) - }*/ + ): SuspendableEvent,Throwable> = SuspendableEvent { val searchURL = search_base_url + query val results = mutableListOf() - try { - (globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach { - (it as? JsonObject)?.formatData()?.let { jsonObject -> + + (globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject) + .getJsonObject("songs") + .getJsonArray("data").requireNotNull().forEach { + (it as JsonObject).formatData().let { jsonObject -> results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject)) } - } - }catch (e: ServerResponseException) {} - return results + } + + results } - suspend fun getLyrics(ID: String): String? { - return try { - (Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject) - .getString("lyrics") - }catch (e:Exception) { null } + suspend fun getLyrics(ID: String): SuspendableEvent = SuspendableEvent { + (Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject) + .getString("lyrics").requireNotNull() } suspend fun getSong( URL: String, fetchLyrics: Boolean = false - ): SaavnSong { + ): SuspendableEvent = SuspendableEvent { val id = getSongID(URL) val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject) .formatData(fetchLyrics) - return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data) + + globalJson.decodeFromJsonElement(SaavnSong.serializer(), data) } suspend fun getSongFromID( ID: String, fetchLyrics: Boolean = false - ): SaavnSong { + ): SuspendableEvent = SuspendableEvent { val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject) .formatData(fetchLyrics) - return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data) + + globalJson.decodeFromJsonElement(SaavnSong.serializer(), data) } private suspend fun getSongID( @@ -105,24 +109,19 @@ interface JioSaavnRequests { suspend fun getPlaylist( URL: String, includeLyrics: Boolean = false - ): SaavnPlaylist? { - return try { - globalJson.decodeFromJsonElement( - SaavnPlaylist.serializer(), - (globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject) - .formatData(includeLyrics) - ) - } catch (e: Exception) { - e.printStackTrace() - null - } + ): SuspendableEvent = SuspendableEvent { + globalJson.decodeFromJsonElement( + SaavnPlaylist.serializer(), + (globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject) + .formatData(includeLyrics) + ) } private suspend fun getPlaylistID( URL: String - ): String { + ): SuspendableEvent = SuspendableEvent { val res = httpClient.get(URL) - return try { + try { res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0] } catch (e: IndexOutOfBoundsException) { res.split("\"page_id\",\"")[1].split("\",\"")[0] @@ -132,24 +131,19 @@ interface JioSaavnRequests { suspend fun getAlbum( URL: String, includeLyrics: Boolean = false - ): SaavnAlbum? { - return try { - globalJson.decodeFromJsonElement( - SaavnAlbum.serializer(), - (globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL))) as JsonObject) - .formatData(includeLyrics) - ) - } catch (e: Exception) { - e.printStackTrace() - null - } + ): SuspendableEvent = SuspendableEvent { + globalJson.decodeFromJsonElement( + SaavnAlbum.serializer(), + (globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject) + .formatData(includeLyrics) + ) } private suspend fun getAlbumID( URL: String - ): String { + ): SuspendableEvent = SuspendableEvent { val res = httpClient.get(URL) - return try { + try { res.split("\"album_id\":\"")[1].split('"')[0] } catch (e: IndexOutOfBoundsException) { res.split("\"page_id\",\"")[1].split("\",\"")[0] @@ -215,8 +209,10 @@ interface JioSaavnRequests { // Fetch Lyrics if Requested // Lyrics is HTML Based if (includeLyrics) { - if (getBoolean("has_lyrics") == true) { - put("lyrics", getString("id")?.let { getLyrics(it) }) + if (getBoolean("has_lyrics") == true && containsKey("id")) { + getLyrics(getString("id").requireNotNull()).success { + put("lyrics", it) + } } else { put("lyrics", "") } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt index 0bd18414..33d24a0d 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt @@ -17,6 +17,8 @@ package com.shabinder.common.di.spotify import com.shabinder.common.di.globalJson +import com.shabinder.common.models.SpotiFlyerException +import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.methods import com.shabinder.common.models.spotify.TokenData import io.ktor.client.* @@ -29,15 +31,12 @@ import io.ktor.client.request.forms.* import io.ktor.http.* import kotlin.native.concurrent.SharedImmutable -suspend fun authenticateSpotify(): TokenData? { - return try { - if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") { +suspend fun authenticateSpotify(): SuspendableEvent = SuspendableEvent { + if (methods.value.isInternetAvailable) { + spotifyAuthClient.post("https://accounts.spotify.com/api/token") { body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") }) - } else null - } catch (e: Exception) { - e.printStackTrace() - null - } + } + } else throw SpotiFlyerException.NoInternetException() } @SharedImmutable diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt index f965c6bc..5bac8eff 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt @@ -19,6 +19,8 @@ package com.shabinder.common.di.youtubeMp3 import co.touchlab.kermit.Kermit import com.shabinder.common.models.corsApi import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.flatMap +import com.shabinder.common.models.event.coroutines.map import com.shabinder.common.requireNotNull import io.ktor.client.* import io.ktor.client.request.* @@ -39,12 +41,11 @@ interface Yt1sMp3 { /* * Downloadable Mp3 Link for YT videoID. * */ - suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent = SuspendableEvent { - getConvertedMp3Link( - videoID, - getKey(videoID).value - ).value["dlink"].requireNotNull() - .jsonPrimitive.content.replace("\"", "") + suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent = getKey(videoID).flatMap { key -> + getConvertedMp3Link(videoID, key).map { + it["dlink"].requireNotNull() + .jsonPrimitive.content.replace("\"", "") + } } /* diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index a93b7908..1503b3d7 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -83,7 +83,7 @@ interface SpotiFlyerList { val queryResult: PlatformQueryResult? = null, val link: String = "", val trackList: List = emptyList(), - val errorOccurred: Exception? = null, + val errorOccurred: Throwable? = null, val askForDonation: Boolean = false, ) } diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index e1368c05..7fddc9c2 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -59,7 +59,7 @@ internal class SpotiFlyerListStoreProvider( data class ResultFetched(val result: PlatformQueryResult, val trackList: List) : Result() data class UpdateTrackList(val list: List) : Result() data class UpdateTrackItem(val item: TrackDetails) : Result() - data class ErrorOccurred(val error: Exception) : Result() + data class ErrorOccurred(val error: Throwable) : Result() data class AskForDonation(val isAllowed: Boolean) : Result() } @@ -90,19 +90,17 @@ internal class SpotiFlyerListStoreProvider( override suspend fun executeIntent(intent: Intent, getState: () -> State) { when (intent) { is Intent.SearchLink -> { - try { - val result = fetchQuery.query(link) - if (result != null) { + val resp = fetchQuery.query(link) + resp.fold( + success = { result -> result.trackList = result.trackList.toMutableList() dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))) executeIntent(Intent.RefreshTracksStatuses, getState) - } else { - throw Exception("An Error Occurred, Check your Link / Connection") + }, + failure = { + dispatch(Result.ErrorOccurred(it)) } - } catch (e: Exception) { - e.printStackTrace() - dispatch(Result.ErrorOccurred(e)) - } + ) } is Intent.StartDownloadAll -> { diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index d322659a..d2b5fb72 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -28,7 +28,7 @@ import com.arkivanov.decompose.statekeeper.Parcelable import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.value.Value import com.shabinder.common.di.Dir -import com.shabinder.common.di.providers.SpotifyProvider +import com.shabinder.common.di.dispatcherIO import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.models.Actions @@ -39,7 +39,6 @@ import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Dependencies import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -77,8 +76,8 @@ internal class SpotiFlyerRootImpl( ) { instanceKeeper.ensureNeverFrozen() methods.value = dependencies.actions.freeze() - /*Authenticate Spotify Client*/ - authenticateSpotify(dependencies.fetchPlatformQueryResult.spotifyProvider) + /*Init App Launch & Authenticate Spotify Client*/ + initAppLaunchAndAuthenticateSpotify(dependencies.fetchPlatformQueryResult::authenticateSpotifyClient) } private val router = @@ -129,11 +128,11 @@ internal class SpotiFlyerRootImpl( } } - private fun authenticateSpotify(spotifyProvider: SpotifyProvider) { - GlobalScope.launch(Dispatchers.Default) { + private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) { + GlobalScope.launch(dispatcherIO) { analytics.appLaunchEvent() /*Authenticate Spotify Client*/ - spotifyProvider.authenticateSpotifyClient() + authenticator() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8cf289b8..3748628b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,11 @@ include( ":console-app", ":maintenance-tasks" ) + +includeBuild("mosaic") { + dependencySubstitution { + substitute(module("com.jakewharton.mosaic:mosaic-gradle-plugin")).with(project(":mosaic-gradle-plugin")) + substitute(module("com.jakewharton.mosaic:mosaic-runtime")).with(project(":mosaic-runtime")) + substitute(module("com.jakewharton.mosaic:compose-compiler")).with(project(":compose:compiler")) + } +} From 7da3147b69697a360776f5dedf25d7973a3cf003 Mon Sep 17 00:00:00 2001 From: shabinder Date: Mon, 21 Jun 2021 21:40:46 +0530 Subject: [PATCH 04/15] Fixes --- .../models/event/coroutines/SuspendableEvent.kt | 14 ++++++-------- .../shabinder/common/di/providers/YoutubeMp3.kt | 7 +++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt index 6e474800..0925d06d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt @@ -8,9 +8,9 @@ inline fun SuspendableEvent<*, *>.getAs() = when (this) { is SuspendableEvent.Failure -> error as? X } -suspend inline fun SuspendableEvent.success(crossinline f: suspend (V) -> Unit) = fold(f, {}) +suspend inline fun SuspendableEvent.success(noinline f: suspend (V) -> Unit) = fold(f, {}) -suspend inline fun SuspendableEvent<*, E>.failure(crossinline f: suspend (E) -> Unit) = fold({}, f) +suspend inline fun SuspendableEvent<*, E>.failure(noinline f: suspend (E) -> Unit) = fold({}, f) infix fun SuspendableEvent.or(fallback: V) = when (this) { is SuspendableEvent.Success -> this @@ -105,7 +105,7 @@ sealed class SuspendableEvent: ReadOnlyProperty abstract operator fun component1(): V? abstract operator fun component2(): E? - suspend inline fun fold(crossinline success: suspend (V) -> X, crossinline failure: suspend (E) -> X): X { + suspend inline fun fold(noinline success: suspend (V) -> X, noinline failure: suspend (E) -> X): X { return when (this) { is Success -> success(this.value) is Failure -> failure(this.error) @@ -164,11 +164,9 @@ sealed class SuspendableEvent: ReadOnlyProperty Failure(ex as E) } - suspend inline operator fun invoke(crossinline block: suspend () -> V): SuspendableEvent = try { - Success(block()) - } catch (ex: Throwable) { - Failure(ex) - } + suspend inline operator fun invoke( + crossinline block: suspend () -> V + ): SuspendableEvent = of(block) } } \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt index 582f492a..03d0c758 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt @@ -17,7 +17,6 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit -import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.youtubeMp3.Yt1sMp3 import com.shabinder.common.models.corsApi import com.shabinder.common.models.event.coroutines.SuspendableEvent @@ -30,9 +29,9 @@ interface YoutubeMp3: Yt1sMp3 { operator fun invoke( client: HttpClient, logger: Kermit - ): AudioToMp3 { - return object : AudioToMp3 { - override val client: HttpClient = client + ): YoutubeMp3 { + return object : YoutubeMp3 { + override val httpClient: HttpClient = client override val logger: Kermit = logger } } From 9b447c3a9dbfc3b5342014217795a35d2d4e05de Mon Sep 17 00:00:00 2001 From: shabinder Date: Tue, 22 Jun 2021 00:30:28 +0530 Subject: [PATCH 05/15] SuspendableEvent and other Deprecated Code Fixes --- .../com/shabinder/spotiflyer/MainActivity.kt | 94 ++++++++++--------- .../shabinder/common/models/event/Event.kt | 2 +- .../event/coroutines/SuspendableEvent.kt | 6 +- .../shabinder/common/di/saavn/JsonUtils.kt | 4 +- .../common/di/utils/ParallelExecutor.kt | 6 +- .../list/store/SpotiFlyerListStoreProvider.kt | 4 +- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 20382daa..1344566c 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -86,6 +86,7 @@ class MainActivity : ComponentActivity() { private lateinit var queryReceiver: BroadcastReceiver private val internetAvailability by lazy { ConnectionLiveData(applicationContext) } private val tracker get() = (application as App).tracker + private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -149,9 +150,7 @@ class MainActivity : ComponentActivity() { } private fun initialise() { - val isGithubRelease = checkAppSignature(this).also { - Log.i("SpotiFlyer Github Rel.:",it.toString()) - } + val isGithubRelease = checkAppSignature(this) /* * Only Send an `Update Notification` on Github Release Builds * and Track Downloads for all other releases like F-Droid, @@ -170,42 +169,6 @@ class MainActivity : ComponentActivity() { return internetAvailability.observeAsState() } - @Suppress("DEPRECATION") - private fun setUpOnPrefClickListener() { - // Initialize Builder - val chooser = StorageChooser.Builder() - .withActivity(this) - .withFragmentManager(fragmentManager) - .withMemoryBar(true) - .setTheme(StorageChooser.Theme(applicationContext).apply { - scheme = applicationContext.resources.getIntArray(R.array.default_dark) - }) - .setDialogTitle("Set Download Directory") - .allowCustomPath(true) - .setType(StorageChooser.DIRECTORY_CHOOSER) - .build() - - // get path that the user has chosen - chooser.setOnSelectListener { path -> - Log.d("Setting Base Path", path) - val f = File(path) - if (f.canWrite()) { - // hell yeah :) - dir.setDownloadDirectory(path) - showPopUpMessage( - "Download Directory Set to:\n${dir.defaultDir()} " - ) - }else{ - showPopUpMessage( - "NO WRITE ACCESS on \n$path ,\nReverting Back to Previous" - ) - } - } - - // Show dialog whenever you want by - chooser.show() - } - private fun showPopUpMessage(string: String, long: Boolean = false) { android.widget.Toast.makeText( applicationContext, @@ -256,12 +219,7 @@ class MainActivity : ComponentActivity() { override fun setDownloadDirectoryAction() = setUpOnPrefClickListener() - override fun queryActiveTracks() { - val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply { - action = "query" - } - ContextCompat.startForegroundService(this@MainActivity, serviceIntent) - } + override fun queryActiveTracks() = this@MainActivity.queryActiveTracks() override fun giveDonation() { openPlatform("",platformLink = "https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button") @@ -337,6 +295,48 @@ class MainActivity : ComponentActivity() { } ) + private fun queryActiveTracks() { + val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply { + action = "query" + } + ContextCompat.startForegroundService(this@MainActivity, serviceIntent) + } + + @Suppress("DEPRECATION") + private fun setUpOnPrefClickListener() { + // Initialize Builder + val chooser = StorageChooser.Builder() + .withActivity(this) + .withFragmentManager(fragmentManager) + .withMemoryBar(true) + .setTheme(StorageChooser.Theme(applicationContext).apply { + scheme = applicationContext.resources.getIntArray(R.array.default_dark) + }) + .setDialogTitle("Set Download Directory") + .allowCustomPath(true) + .setType(StorageChooser.DIRECTORY_CHOOSER) + .build() + + // get path that the user has chosen + chooser.setOnSelectListener { path -> + Log.d("Setting Base Path", path) + val f = File(path) + if (f.canWrite()) { + // hell yeah :) + dir.setDownloadDirectory(path) + showPopUpMessage( + "Download Directory Set to:\n${dir.defaultDir()} " + ) + }else{ + showPopUpMessage( + "NO WRITE ACCESS on \n$path ,\nReverting Back to Previous" + ) + } + } + + // Show dialog whenever you want by + chooser.show() + } @SuppressLint("ObsoleteSdkInt") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -418,6 +418,10 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() initializeBroadcast() + if(visibleChild is SpotiFlyerRoot.Child.List) { + // Update Track List Statuses when Returning to App + queryActiveTracks() + } } override fun onPause() { diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt index 76cfb021..0997ab96 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt @@ -161,7 +161,7 @@ sealed class Event: ReadOnlyProperty { class Failure(val error: E) : Event() { override fun component2(): E = error - override val value: Nothing = throw error + override val value: Nothing get() = throw error fun getThrowable(): E = error diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt index 0925d06d..bd6485b2 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt @@ -134,7 +134,7 @@ sealed class SuspendableEvent: ReadOnlyProperty override fun component1(): V? = null override fun component2(): E? = error - override val value: V = throw error + override val value: V get() = throw error fun getThrowable(): E = error @@ -158,7 +158,9 @@ sealed class SuspendableEvent: ReadOnlyProperty return value?.let { Success(it) } ?: error(fail()) } - suspend inline fun of(crossinline block: suspend () -> V): SuspendableEvent = try { + suspend inline fun of( + crossinline block: suspend () -> V + ): SuspendableEvent = try { Success(block()) } catch (ex: Throwable) { Failure(ex as E) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt index eb845e0e..367f39a1 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt @@ -6,7 +6,7 @@ package com.shabinder.common.di.saavn fun String.escape(): String { val output = StringBuilder() for (element in this) { - val chx = element.toInt() + val chx = element.code if (chx != 0) { when (element) { '\n' -> { @@ -76,7 +76,7 @@ fun String.unescape(): String { /*if (!x.isLetterOrDigit()) { throw RuntimeException("Bad character in unicode escape.") }*/ - hex.append(x.toLowerCase()) + hex.append(x.lowercaseChar()) } i += 4 // consume those four digits. val code = hex.toString().toInt(16) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt index 13aa686a..7fa50889 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt @@ -22,7 +22,7 @@ package com.shabinder.common.di.utils // Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e import com.shabinder.common.di.dispatcherIO -import io.ktor.utils.io.core.Closeable +import io.ktor.utils.io.core.* import kotlinx.atomicfu.atomic import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -96,7 +96,7 @@ class ParallelExecutor( return var change = expectedCount - actualCount - while (change > 0 && killQueue.poll() != null) + while (change > 0 && killQueue.tryReceive().getOrNull() != null) change -= 1 if (change > 0) @@ -104,7 +104,7 @@ class ParallelExecutor( repeat(change) { launchProcessor() } } else - repeat(-change) { killQueue.offer(Unit) } + repeat(-change) { killQueue.trySend(Unit).isSuccess } } private class Operation( diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index 7fddc9c2..c1459231 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -33,7 +33,7 @@ import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.collect internal class SpotiFlyerListStoreProvider( private val dir: Dir, @@ -80,7 +80,7 @@ internal class SpotiFlyerListStoreProvider( ) } - downloadProgressFlow.collectLatest { map -> + downloadProgressFlow.collect { map -> logger.d(map.size.toString(), "ListStore: flow Updated") val updatedTrackList = getState().trackList.updateTracksStatuses(map) if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList)) From 979fcc342b5cf80bb7307f546c43a2a577b8cb88 Mon Sep 17 00:00:00 2001 From: shabinder Date: Tue, 22 Jun 2021 11:43:30 +0530 Subject: [PATCH 06/15] - Removed BroadcastReceivers and Bound to Service instead, - Code Improv and Cleanup --- android/build.gradle.kts | 3 +- android/src/main/AndroidManifest.xml | 2 +- .../com/shabinder/spotiflyer/MainActivity.kt | 170 +++++++++--------- .../spotiflyer/service}/ForegroundService.kt | 122 ++++++------- .../spotiflyer/service/TrackStatusFlowMap.kt | 17 ++ .../shabinder/spotiflyer/service}/Utils.kt | 13 +- .../common/uikit/SpotiFlyerListUi.kt | 27 ++- .../AndroidPlatformActions.kt | 4 +- .../kotlin/com/shabinder/common/di/Dir.kt | 10 +- .../list/integration/SpotiFlyerListImpl.kt | 4 + .../list/store/SpotiFlyerListStoreProvider.kt | 8 +- 11 files changed, 204 insertions(+), 176 deletions(-) rename {common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker => android/src/main/java/com/shabinder/spotiflyer/service}/ForegroundService.kt (76%) create mode 100644 android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt rename {common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker => android/src/main/java/com/shabinder/spotiflyer/service}/Utils.kt (61%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dfd33500..8f935e16 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -121,13 +121,14 @@ dependencies { implementation(MVIKotlin.mvikotlinTimeTravel) // Extras - Extras.Android.apply { + with(Extras.Android) { implementation(Acra.notification) implementation(Acra.http) implementation(appUpdator) implementation(matomo) } + implementation(Extras.kermit) //implementation("com.jakewharton.timber:timber:4.7.1") implementation("dev.icerock.moko:parcelize:0.7.0") implementation("com.github.shabinder:storage-chooser:2.0.4.45") diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 5fa70abf..04d4f636 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -72,6 +72,6 @@ - + \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 1344566c..0bd21b8d 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -17,15 +17,16 @@ package com.shabinder.spotiflyer import android.annotation.SuppressLint -import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.IntentFilter +import android.content.ServiceConnection import android.content.pm.PackageManager import android.media.MediaScannerConnection import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.IBinder import android.os.PowerManager import android.util.Log import androidx.activity.ComponentActivity @@ -51,18 +52,17 @@ import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsHeight import com.google.accompanist.insets.statusBarsPadding import com.shabinder.common.di.* -import com.shabinder.common.di.worker.ForegroundService import com.shabinder.common.models.Actions import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey -import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.uikit.* +import com.shabinder.spotiflyer.service.ForegroundService import com.shabinder.spotiflyer.ui.AnalyticsDialog import com.shabinder.spotiflyer.ui.NetworkDialog import com.shabinder.spotiflyer.ui.PermissionDialog @@ -82,12 +82,16 @@ class MainActivity : ComponentActivity() { private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks private val trackStatusFlow = MutableSharedFlow>(1) private var permissionGranted = mutableStateOf(true) - private lateinit var updateUIReceiver: BroadcastReceiver - private lateinit var queryReceiver: BroadcastReceiver private val internetAvailability by lazy { ConnectionLiveData(applicationContext) } private val tracker get() = (application as App).tracker private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance + // Variable for storing instance of our service class + var foregroundService: ForegroundService? = null + + // Boolean to check if our activity is bound to service or not + var isServiceBound: Boolean? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This app draws behind the system bars, so we want to handle fitting system windows @@ -162,8 +166,62 @@ class MainActivity : ComponentActivity() { TrackHelper.track().download().with(tracker) } handleIntentFromExternalActivity() + + initForegroundService() } + /*START: Foreground Service Handlers*/ + private fun initForegroundService() { + // Start and then Bind to the Service + ContextCompat.startForegroundService( + this@MainActivity, + Intent(this, ForegroundService::class.java) + ) + bindService() + } + + /** + * Interface for getting the instance of binder from our service class + * So client can get instance of our service class and can directly communicate with it. + */ + private val serviceConnection = object : ServiceConnection { + val tag = "Service Connection" + + override fun onServiceConnected(className: ComponentName, iBinder: IBinder) { + Log.d(tag, "connected to service.") + // We've bound to MyService, cast the IBinder and get MyBinder instance + val binder = iBinder as ForegroundService.DownloadServiceBinder + foregroundService = binder.service + isServiceBound = true + lifecycleScope.launch { + foregroundService?.trackStatusFlowMap?.statusFlow?.let { + trackStatusFlow.emitAll(it.conflate()) + } + } + } + + override fun onServiceDisconnected(arg0: ComponentName) { + Log.d(tag, "disconnected from service.") + isServiceBound = false + } + } + + /*Used to bind to our service class*/ + private fun bindService() { + Intent(this, ForegroundService::class.java).also { intent -> + bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + } + } + + /*Used to unbind from our service class*/ + private fun unbindService() { + Intent(this, ForegroundService::class.java).also { + unbindService(serviceConnection) + } + } + /*END: Foreground Service Handlers*/ + + @Composable private fun isInternetAvailableState(): State { return internetAvailability.observeAsState() @@ -206,12 +264,9 @@ class MainActivity : ComponentActivity() { ) } - override fun sendTracksToService(array: ArrayList) { - for (list in array.chunked(50)) { - val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java) - serviceIntent.putParcelableArrayListExtra("object", list as ArrayList) - ContextCompat.startForegroundService(this@MainActivity, serviceIntent) - } + override fun sendTracksToService(array: List) { + if (foregroundService == null) initForegroundService() + foregroundService?.downloadAllTracks(array) } } @@ -296,10 +351,16 @@ class MainActivity : ComponentActivity() { ) private fun queryActiveTracks() { - val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply { - action = "query" + lifecycleScope.launch { + foregroundService?.trackStatusFlowMap?.let { tracksStatus -> + trackStatusFlow.emit(tracksStatus) + } } - ContextCompat.startForegroundService(this@MainActivity, serviceIntent) + } + + override fun onResume() { + super.onResume() + queryActiveTracks() } @Suppress("DEPRECATION") @@ -357,80 +418,6 @@ class MainActivity : ComponentActivity() { } } - /* - * Broadcast Handlers - * */ - private fun initializeBroadcast(){ - val intentFilter = IntentFilter().apply { - addAction(Status.QUEUED.name) - addAction(Status.FAILED.name) - addAction(Status.DOWNLOADING.name) - addAction(Status.COMPLETED.name) - addAction("Progress") - addAction("Converting") - } - updateUIReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - //Update Flow with latest details - if (intent != null) { - val trackDetails = intent.getParcelableExtra("track") - trackDetails?.let { track -> - lifecycleScope.launch { - val latestMap = trackStatusFlow.replayCache.getOrElse(0 - ) { hashMapOf() }.apply { - this[track.title] = when (intent.action) { - Status.QUEUED.name -> DownloadStatus.Queued - Status.FAILED.name -> DownloadStatus.Failed - Status.DOWNLOADING.name -> DownloadStatus.Downloading() - "Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0)) - "Converting" -> DownloadStatus.Converting - Status.COMPLETED.name -> DownloadStatus.Downloaded - else -> DownloadStatus.NotDownloaded - } - } - trackStatusFlow.emit(latestMap) - Log.i("Track Update",track.title + track.downloaded.toString()) - } - } - } - } - } - val queryFilter = IntentFilter().apply { addAction("query_result") } - queryReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - //UI update here - if (intent != null){ - @Suppress("UNCHECKED_CAST") - val trackList = intent.getSerializableExtra("tracks") as? HashMap? - trackList?.let { list -> - Log.i("Service Response", "${list.size} Tracks Active") - lifecycleScope.launch { - trackStatusFlow.emit(list) - } - } - } - } - } - registerReceiver(updateUIReceiver, intentFilter) - registerReceiver(queryReceiver, queryFilter) - } - - override fun onResume() { - super.onResume() - initializeBroadcast() - if(visibleChild is SpotiFlyerRoot.Child.List) { - // Update Track List Statuses when Returning to App - queryActiveTracks() - } - } - - override fun onPause() { - super.onPause() - unregisterReceiver(updateUIReceiver) - unregisterReceiver(queryReceiver) - } - - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleIntentFromExternalActivity(intent) @@ -455,6 +442,11 @@ class MainActivity : ComponentActivity() { } } + override fun onDestroy() { + super.onDestroy() + unbindService() + } + companion object { const val disableDozeCode = 1223 } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt similarity index 76% rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt rename to android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index f2bf595c..dee184a9 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -14,7 +14,7 @@ * along with this program. If not, see . */ -package com.shabinder.common.di.worker +package com.shabinder.spotiflyer.service import android.annotation.SuppressLint import android.app.DownloadManager @@ -26,6 +26,7 @@ import android.app.PendingIntent.FLAG_CANCEL_CURRENT import android.app.Service import android.content.Context import android.content.Intent +import android.os.Binder import android.os.Build import android.os.IBinder import android.os.PowerManager @@ -40,12 +41,13 @@ import com.shabinder.common.di.downloadFile import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus -import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.koin.android.ext.android.inject @@ -68,7 +70,8 @@ class ForegroundService : Service(), CoroutineScope { override val coroutineContext: CoroutineContext get() = serviceJob + Dispatchers.IO - private val allTracksStatus = hashMapOf() + val trackStatusFlowMap = TrackStatusFlowMap(MutableSharedFlow(replay = 1),this) + private var messageList = mutableListOf("", "", "", "", "") private var wakeLock: PowerManager.WakeLock? = null private var isServiceStarted = false @@ -80,7 +83,16 @@ class ForegroundService : Service(), CoroutineScope { private val logger: Kermit by inject() private val dir: Dir by inject() - override fun onBind(intent: Intent): IBinder? = null + + inner class DownloadServiceBinder : Binder() { + // Return this instance of MyService so clients can call public methods + val service: ForegroundService + get() =// Return this instance of Foreground Service so clients can call public methods + this@ForegroundService + } + private val myBinder: IBinder = DownloadServiceBinder() + + override fun onBind(intent: Intent): IBinder = myBinder @SuppressLint("UnspecifiedImmutableFlag") override fun onCreate() { @@ -110,31 +122,13 @@ class ForegroundService : Service(), CoroutineScope { "query" -> { val response = Intent().apply { action = "query_result" - synchronized(allTracksStatus) { - putExtra("tracks", allTracksStatus) + synchronized(trackStatusFlowMap) { + putExtra("tracks", trackStatusFlowMap) } } sendBroadcast(response) } } - - val downloadObjects: ArrayList? = ( - it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList( - "object" - ) - ) - - downloadObjects?.let { list -> - downloadObjects.size.let { size -> - total += size - isSingleDownload = (size == 1) - } - list.forEach { track -> - allTracksStatus[track.title] = DownloadStatus.Queued - } - updateNotification() - downloadAllTracks(list) - } } // Wake locks and misc tasks from here : return if (isServiceStarted) { @@ -156,8 +150,16 @@ class ForegroundService : Service(), CoroutineScope { /** * Function To Download All Tracks Available in a List **/ - private fun downloadAllTracks(trackList: List) { + fun downloadAllTracks(trackList: List) { + + trackList.size.also { size -> + total += size + isSingleDownload = (size == 1) + updateNotification() + } + trackList.forEach { + trackStatusFlowMap[it.title] = DownloadStatus.Queued launch(Dispatchers.IO) { downloadService.execute { fetcher.findMp3DownloadLink(it).fold( @@ -165,10 +167,9 @@ class ForegroundService : Service(), CoroutineScope { enqueueDownload(url, it) }, failure = { _ -> - sendTrackBroadcast(Status.FAILED.name, it) failed++ updateNotification() - allTracksStatus[it.title] = DownloadStatus.Failed + trackStatusFlowMap[it.title] = DownloadStatus.Failed } ) } @@ -180,24 +181,20 @@ class ForegroundService : Service(), CoroutineScope { // Initiating Download addToNotification("Downloading ${track.title}") logger.d(tag) { "${track.title} Download Started" } - allTracksStatus[track.title] = DownloadStatus.Downloading() - sendTrackBroadcast(Status.DOWNLOADING.name, track) + trackStatusFlowMap[track.title] = DownloadStatus.Downloading() // Enqueueing Download downloadFile(url).collect { when (it) { is DownloadResult.Error -> { - launch { - logger.d(tag) { it.message } - removeFromNotification("Downloading ${track.title}") - failed++ - updateNotification() - sendTrackBroadcast(Status.FAILED.name, track) - } + logger.d(tag) { it.message } + removeFromNotification("Downloading ${track.title}") + failed++ + updateNotification() } is DownloadResult.Progress -> { - allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress) + trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) logger.d(tag) { "${track.title} Progress: ${it.progress} %" } val intent = Intent().apply { @@ -209,26 +206,31 @@ class ForegroundService : Service(), CoroutineScope { } is DownloadResult.Success -> { - try { - // Save File and Embed Metadata - val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} } - allTracksStatus[track.title] = DownloadStatus.Converting - sendTrackBroadcast("Converting", track) - addToNotification("Processing ${track.title}") - job.invokeOnCompletion { - converted++ - allTracksStatus[track.title] = DownloadStatus.Downloaded - sendTrackBroadcast(Status.COMPLETED.name, track) - removeFromNotification("Processing ${track.title}") + coroutineScope { + try { + // Save File and Embed Metadata + val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} } + + // Send Converting Status + trackStatusFlowMap[track.title] = DownloadStatus.Converting + addToNotification("Processing ${track.title}") + + // All Processing Completed for this Track + job.invokeOnCompletion { + converted++ + trackStatusFlowMap[track.title] = DownloadStatus.Downloaded + removeFromNotification("Processing ${track.title}") + } + logger.d(tag) { "${track.title} Download Completed" } + downloaded++ + } catch (e: Exception) { + e.printStackTrace() + // Download Failed + logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" } + failed++ } - logger.d(tag) { "${track.title} Download Completed" } - downloaded++ - } catch (e: Exception) { - // Download Failed - logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" } - failed++ + removeFromNotification("Downloading ${track.title}") } - removeFromNotification("Downloading ${track.title}") } } } @@ -270,7 +272,7 @@ class ForegroundService : Service(), CoroutineScope { messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") downloadService.close() updateNotification() - cleanFiles(File(dir.defaultDir()), logger) + cleanFiles(File(dir.defaultDir())) // TODO cleanFiles(File(dir.imageCacheDir())) messageList = mutableListOf("", "", "", "", "") releaseWakeLock() @@ -336,12 +338,4 @@ class ForegroundService : Service(), CoroutineScope { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager mNotificationManager.notify(notificationId, getNotification()) } - - private fun sendTrackBroadcast(action: String, track: TrackDetails) { - val intent = Intent().apply { - setAction(action) - putExtra("track", track) - } - this@ForegroundService.sendBroadcast(intent) - } } diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt b/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt new file mode 100644 index 00000000..be4bcab7 --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt @@ -0,0 +1,17 @@ +package com.shabinder.spotiflyer.service + +import com.shabinder.common.models.DownloadStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +class TrackStatusFlowMap( + val statusFlow: MutableSharedFlow>, + private val scope: CoroutineScope +): HashMap() { + override fun put(key: String, value: DownloadStatus): DownloadStatus? { + val res = super.put(key, value) + scope.launch { statusFlow.emit(this@TrackStatusFlowMap) } + return res + } +} \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt similarity index 61% rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt rename to android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt index 06c26c80..309e68fd 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt @@ -1,22 +1,22 @@ -package com.shabinder.common.di.worker +package com.shabinder.spotiflyer.service -import co.touchlab.kermit.Kermit +import android.util.Log import java.io.File /** * Cleaning All Residual Files except Mp3 Files **/ -fun cleanFiles(dir: File, logger: Kermit) { +fun cleanFiles(dir: File) { try { - logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " } + Log.d("File Cleaning","Starting Cleaning in ${dir.path} ") val fList = dir.listFiles() fList?.let { for (file in fList) { if (file.isDirectory) { - cleanFiles(file, logger) + cleanFiles(file) } else if (file.isFile) { if (file.path.toString().substringAfterLast(".") != "mp3") { - logger.d("Files Cleaning") { "Cleaning ${file.path}" } + Log.d("Files Cleaning","Cleaning ${file.path}") file.delete() } } @@ -24,3 +24,4 @@ fun cleanFiles(dir: File, logger: Kermit) { } } catch (e: Exception) { e.printStackTrace() } } + diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt index af210499..72dce041 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt @@ -17,12 +17,32 @@ package com.shabinder.common.uikit import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExtendedFloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -53,6 +73,7 @@ fun SpotiFlyerListContent( component.onBackPressed() } } + Box(modifier = modifier.fillMaxSize()) { val result = model.queryResult if (result == null) { diff --git a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt index 6fd52fd8..0d6adde2 100644 --- a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt +++ b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt @@ -14,7 +14,7 @@ actual interface PlatformActions { fun addToLibrary(path: String) - fun sendTracksToService(array: ArrayList) + fun sendTracksToService(array: List) } actual val StubPlatformActions = object : PlatformActions { @@ -24,5 +24,5 @@ actual val StubPlatformActions = object : PlatformActions { override fun addToLibrary(path: String) {} - override fun sendTracksToService(array: ArrayList) {} + override fun sendTracksToService(array: List) {} } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index ad7d8bf9..7820eb92 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -23,11 +23,9 @@ import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.TrackDetails import com.shabinder.database.Database -import io.ktor.client.request.HttpRequestBuilder -import io.ktor.client.request.get -import io.ktor.client.statement.HttpStatement -import io.ktor.http.contentLength -import io.ktor.http.isSuccess +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlin.math.roundToInt @@ -105,7 +103,7 @@ suspend fun downloadFile(url: String): Flow { var offset = 0 do { // Set Length optimally, after how many kb you want a progress update, now it 0.25mb - val currentRead = response.content.readAvailable(data, offset, 250000) + val currentRead = response.content.readAvailable(data, offset, 2_50_000) offset += currentRead val progress = (offset * 100f / data.size).roundToInt() emit(DownloadResult.Progress(progress)) diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 642a3443..ede4efee 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -18,6 +18,7 @@ package com.shabinder.common.list.integration import co.touchlab.stately.ensureNeverFrozen import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.lifecycle.doOnResume import com.arkivanov.decompose.value.Value import com.shabinder.common.caching.Cache import com.shabinder.common.di.Picture @@ -38,6 +39,9 @@ internal class SpotiFlyerListImpl( init { instanceKeeper.ensureNeverFrozen() + lifecycle.doOnResume { + onRefreshTracksStatuses() + } } private val store = diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index c1459231..5d9e148a 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -60,7 +60,7 @@ internal class SpotiFlyerListStoreProvider( data class UpdateTrackList(val list: List) : Result() data class UpdateTrackItem(val item: TrackDetails) : Result() data class ErrorOccurred(val error: Throwable) : Result() - data class AskForDonation(val isAllowed: Boolean) : Result() + data class AskForSupport(val isAllowed: Boolean) : Result() } private inner class ExecutorImpl : SuspendExecutor() { @@ -73,7 +73,7 @@ internal class SpotiFlyerListStoreProvider( logger.d(message = "Database List Last ID: $it", tag = "Database Last ID") val offset = dir.getDonationOffset dispatch( - Result.AskForDonation( + Result.AskForSupport( // Every 3rd Interval or After some offset isAllowed = offset < 4 && (it % offset == 0L) ) @@ -81,7 +81,7 @@ internal class SpotiFlyerListStoreProvider( } downloadProgressFlow.collect { map -> - logger.d(map.size.toString(), "ListStore: flow Updated") + // logger.d(map.size.toString(), "ListStore: flow Updated") val updatedTrackList = getState().trackList.updateTracksStatuses(map) if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList)) } @@ -131,7 +131,7 @@ internal class SpotiFlyerListStoreProvider( is Result.UpdateTrackList -> copy(trackList = result.list) is Result.UpdateTrackItem -> updateTrackItem(result.item) is Result.ErrorOccurred -> copy(errorOccurred = result.error) - is Result.AskForDonation -> copy(askForDonation = result.isAllowed) + is Result.AskForSupport -> copy(askForDonation = result.isAllowed) } private fun State.updateTrackItem(item: TrackDetails): State { From b3abc9c4de28131b057be8730b8f159410572ac3 Mon Sep 17 00:00:00 2001 From: shabinder Date: Tue, 22 Jun 2021 12:53:18 +0530 Subject: [PATCH 07/15] Error Handling Improv --- .../spotiflyer/service/ForegroundService.kt | 44 ++++++------------- .../shabinder/common/models/DownloadObject.kt | 2 +- .../common/models/SpotiFlyerException.kt | 1 + .../kotlin/com/shabinder/common/di/DI.kt | 2 +- .../common/di/FetchPlatformQueryResult.kt | 6 ++- .../common/di/audioToMp3/AudioToMp3.kt | 31 ++++++++----- .../list/store/SpotiFlyerListStoreProvider.kt | 5 +-- 7 files changed, 42 insertions(+), 49 deletions(-) diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index dee184a9..333a8d10 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -42,6 +42,8 @@ import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.event.coroutines.SuspendableEvent +import com.shabinder.common.models.event.coroutines.failure import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -102,10 +104,7 @@ class ForegroundService : Service(), CoroutineScope { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel(channelId, "Downloader Service") } - val intent = Intent( - this, - ForegroundService::class.java - ).apply { action = "kill" } + val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" } cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT) downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager } @@ -119,17 +118,9 @@ class ForegroundService : Service(), CoroutineScope { intent?.let { when (it.action) { "kill" -> killService() - "query" -> { - val response = Intent().apply { - action = "query_result" - synchronized(trackStatusFlowMap) { - putExtra("tracks", trackStatusFlowMap) - } - } - sendBroadcast(response) - } } } + // Wake locks and misc tasks from here : return if (isServiceStarted) { // Service Already Started @@ -160,16 +151,16 @@ class ForegroundService : Service(), CoroutineScope { trackList.forEach { trackStatusFlowMap[it.title] = DownloadStatus.Queued - launch(Dispatchers.IO) { + launch { downloadService.execute { fetcher.findMp3DownloadLink(it).fold( success = { url -> enqueueDownload(url, it) }, - failure = { _ -> + failure = { error -> failed++ updateNotification() - trackStatusFlowMap[it.title] = DownloadStatus.Failed + trackStatusFlowMap[it.title] = DownloadStatus.Failed(error) } ) } @@ -180,7 +171,6 @@ class ForegroundService : Service(), CoroutineScope { private suspend fun enqueueDownload(url: String, track: TrackDetails) { // Initiating Download addToNotification("Downloading ${track.title}") - logger.d(tag) { "${track.title} Download Started" } trackStatusFlowMap[track.title] = DownloadStatus.Downloading() // Enqueueing Download @@ -188,26 +178,18 @@ class ForegroundService : Service(), CoroutineScope { when (it) { is DownloadResult.Error -> { logger.d(tag) { it.message } - removeFromNotification("Downloading ${track.title}") failed++ - updateNotification() + trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message)) + removeFromNotification("Downloading ${track.title}") } is DownloadResult.Progress -> { trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) - logger.d(tag) { "${track.title} Progress: ${it.progress} %" } - - val intent = Intent().apply { - action = "Progress" - putExtra("progress", it.progress) - putExtra("track", track) - } - sendBroadcast(intent) } is DownloadResult.Success -> { coroutineScope { - try { + SuspendableEvent { // Save File and Embed Metadata val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} } @@ -223,11 +205,11 @@ class ForegroundService : Service(), CoroutineScope { } logger.d(tag) { "${track.title} Download Completed" } downloaded++ - } catch (e: Exception) { - e.printStackTrace() + }.failure { error -> + error.printStackTrace() // Download Failed - logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" } failed++ + trackStatusFlowMap[track.title] = DownloadStatus.Failed(error) } removeFromNotification("Downloading ${track.title}") } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt index b0156ae1..f592c95d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt @@ -49,5 +49,5 @@ sealed class DownloadStatus : Parcelable { @Parcelize object Queued : DownloadStatus() @Parcelize object NotDownloaded : DownloadStatus() @Parcelize object Converting : DownloadStatus() - @Parcelize object Failed : DownloadStatus() + @Parcelize data class Failed(val error: Throwable) : DownloadStatus() } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index 65b31c1d..1eb0ad12 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -4,6 +4,7 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message) + data class MP3ConversionFailed(override val message: String = "MP3 Converter unreachable, probably BUSY !"): SpotiFlyerException(message) data class NoMatchFound( val trackName: String? = null, diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index fb531555..0d23dcc0 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -61,7 +61,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module { single { YoutubeProvider(get(), get(), get()) } single { YoutubeMp3(get(), get()) } single { YoutubeMusic(get(), get(), get(), get(), get()) } - single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get()) } + single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) } } @ThreadLocal diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt index 2e7bb311..70e030b0 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt @@ -16,6 +16,7 @@ package com.shabinder.common.di +import co.touchlab.kermit.Kermit import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.providers.GaanaProvider @@ -45,7 +46,8 @@ class FetchPlatformQueryResult( private val youtubeMusic: YoutubeMusic, private val youtubeMp3: YoutubeMp3, private val audioToMp3: AudioToMp3, - val dir: Dir + val dir: Dir, + val logger: Kermit ) { private val db: DownloadRecordDatabaseQueries? get() = dir.db?.downloadRecordDatabaseQueries @@ -120,7 +122,7 @@ class FetchPlatformQueryResult( trackName = track.title, trackArtists = track.artists ).flatMapError { saavnError -> - // Lets Try Fetching Now From Youtube Music + // Saavn Failed, Lets Try Fetching Now From Youtube Music youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError -> // If Both Failed Bubble the Exception Up with both StackTraces SuspendableEvent.error( diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt index 0a81e57a..63845c55 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt @@ -2,8 +2,10 @@ package com.shabinder.common.di.audioToMp3 import co.touchlab.kermit.Kermit import com.shabinder.common.models.AudioQuality +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.event.coroutines.SuspendableEvent import io.ktor.client.* +import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.request.forms.* import io.ktor.client.statement.* @@ -31,8 +33,9 @@ interface AudioToMp3 { URL: String, audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)), ): SuspendableEvent = SuspendableEvent { - val activeHost by getHost() // ex - https://hostveryfast.onlineconverter.com/file/send - val jobLink by convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd + // Active Host ex - https://hostveryfast.onlineconverter.com/file/send + // Convert Job Request ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd + var (activeHost,jobLink) = convertRequest(URL, audioQuality).value // (jobStatus.contains("d")) == COMPLETION var jobStatus: String @@ -44,13 +47,22 @@ interface AudioToMp3 { "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}" ) } catch (e: Exception) { + if(e is ClientRequestException && e.response.status.value == 404) { + // No Need to Retry, Host/Converter is Busy + throw SpotiFlyerException.MP3ConversionFailed() + } + // Try Using New Host/Converter + convertRequest(URL, audioQuality).value.also { + activeHost = it.first + jobLink = it.second + } e.printStackTrace() "" } retryCount-- logger.i("Job Status") { jobStatus } - if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio - } while (!jobStatus.contains("d", true) && retryCount != 0) + if (!jobStatus.contains("d")) delay(600) // Add Delay , to give Server Time to process audio + } while (!jobStatus.contains("d", true) && retryCount > 0) "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download" } @@ -61,11 +73,10 @@ interface AudioToMp3 { * */ private suspend fun convertRequest( URL: String, - host: String? = null, audioQuality: AudioQuality = AudioQuality.KBPS160, - ): SuspendableEvent = SuspendableEvent { - val activeHost = host ?: getHost().value - val res = client.submitFormWithBinaryData( + ): SuspendableEvent,Throwable> = SuspendableEvent { + val activeHost by getHost() + val convertJob = client.submitFormWithBinaryData( url = activeHost, formData = formData { append("class", "audio") @@ -86,14 +97,14 @@ interface AudioToMp3 { dropLast(3) // last 3 are useless unicode char } - val job = client.get(res) { + val job = client.get(convertJob) { headers { header("Host", "www.onlineconverter.com") } }.execute() logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() } - res + Pair(activeHost,convertJob) } // Active Host free to process conversion diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index 5d9e148a..a2f17aa9 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -21,7 +21,6 @@ import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor -import com.shabinder.common.database.getLogger import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.downloadTracks @@ -42,8 +41,6 @@ internal class SpotiFlyerListStoreProvider( private val link: String, private val downloadProgressFlow: MutableSharedFlow> ) { - val logger = getLogger() - fun provide(): SpotiFlyerListStore = object : SpotiFlyerListStore, @@ -70,7 +67,7 @@ internal class SpotiFlyerListStoreProvider( dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also { // See if It's Time we can request for support for maintaining this project or not - logger.d(message = "Database List Last ID: $it", tag = "Database Last ID") + fetchQuery.logger.d(message = { "Database List Last ID: $it" }, tag = "Database Last ID") val offset = dir.getDonationOffset dispatch( Result.AskForSupport( From 6566c35888da242ec6da3052f0d4b987ab3033d8 Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 23 Jun 2021 00:18:01 +0530 Subject: [PATCH 08/15] Service Cleanup, AutoClear & Notification Optimisations --- android/build.gradle.kts | 7 +- .../spotiflyer/service/ForegroundService.kt | 171 +++++++++--------- .../shabinder/spotiflyer/service/Message.kt | 33 ++++ .../spotiflyer/utils/autoclear/AutoClear.kt | 74 ++++++++ .../utils/autoclear/AutoClearFragment.kt | 62 +++++++ .../autoclear/LifecycleAutoInitializer.kt | 7 + .../LifecycleCreateAndDestroyObserver.kt | 21 +++ .../LifecycleResumeAndPauseObserver.kt | 21 +++ .../LifecycleStartAndStopObserver.kt | 21 +++ buildSrc/buildSrc/src/main/kotlin/Versions.kt | 2 +- .../kotlin/com/shabinder/common/Ext.kt | 2 +- .../common/models/SpotiFlyerException.kt | 6 +- .../common/di/FetchPlatformQueryResult.kt | 1 + .../common/di/audioToMp3/AudioToMp3.kt | 4 +- 14 files changed, 343 insertions(+), 89 deletions(-) create mode 100644 android/src/main/java/com/shabinder/spotiflyer/service/Message.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt create mode 100644 android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 8f935e16..ab8e3ac0 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -128,11 +128,16 @@ dependencies { implementation(matomo) } + with(Versions.androidxLifecycle) { + implementation("androidx.lifecycle:lifecycle-service:$this") + implementation("androidx.lifecycle:lifecycle-common-java8:$this") + } + implementation(Extras.kermit) //implementation("com.jakewharton.timber:timber:4.7.1") implementation("dev.icerock.moko:parcelize:0.7.0") implementation("com.github.shabinder:storage-chooser:2.0.4.45") - implementation("com.google.accompanist:accompanist-insets:0.11.1") + implementation("com.google.accompanist:accompanist-insets:0.12.0") // Test testImplementation("junit:junit:4.13.2") diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index 333a8d10..f482f4ea 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -17,13 +17,11 @@ package com.shabinder.spotiflyer.service import android.annotation.SuppressLint -import android.app.DownloadManager import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_CANCEL_CURRENT -import android.app.Service import android.content.Context import android.content.Intent import android.os.Binder @@ -31,8 +29,9 @@ import android.os.Build import android.os.IBinder import android.os.PowerManager import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope import co.touchlab.kermit.Kermit import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult @@ -44,23 +43,33 @@ import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.failure -import kotlinx.coroutines.CoroutineScope +import com.shabinder.spotiflyer.utils.autoclear.AutoClear +import com.shabinder.spotiflyer.utils.autoclear.autoClear import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import java.io.File -import kotlin.coroutines.CoroutineContext -class ForegroundService : Service(), CoroutineScope { +class ForegroundService : LifecycleService() { - private val tag: String = "Foreground Service" - private val channelId = "ForegroundDownloaderService" - private val notificationId = 101 + val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) } + private var downloadService: AutoClear = autoClear { ParallelExecutor(Dispatchers.IO) } + private val fetcher: FetchPlatformQueryResult by inject() + private val logger: Kermit by inject() + private val dir: Dir by inject() + + private var messageList = MutableList(5) { emptyMessage } + private var wakeLock: PowerManager.WakeLock? = null + private var isServiceStarted = false + private val cancelIntent: PendingIntent by lazy { + val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" } + PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT) + } + + /* Variables Holding Download State */ private var total = 0 // Total Downloads Requested private var converted = 0 // Total Files Converted private var downloaded = 0 // Total Files downloaded @@ -68,52 +77,27 @@ class ForegroundService : Service(), CoroutineScope { private val isFinished get() = converted + failed == total private var isSingleDownload = false - private lateinit var serviceJob: Job - override val coroutineContext: CoroutineContext - get() = serviceJob + Dispatchers.IO - - val trackStatusFlowMap = TrackStatusFlowMap(MutableSharedFlow(replay = 1),this) - - private var messageList = mutableListOf("", "", "", "", "") - private var wakeLock: PowerManager.WakeLock? = null - private var isServiceStarted = false - private lateinit var cancelIntent: PendingIntent - - private lateinit var downloadManager: DownloadManager - private lateinit var downloadService: ParallelExecutor - private val fetcher: FetchPlatformQueryResult by inject() - private val logger: Kermit by inject() - private val dir: Dir by inject() - - inner class DownloadServiceBinder : Binder() { - // Return this instance of MyService so clients can call public methods - val service: ForegroundService - get() =// Return this instance of Foreground Service so clients can call public methods - this@ForegroundService + val service get() = this@ForegroundService } private val myBinder: IBinder = DownloadServiceBinder() - override fun onBind(intent: Intent): IBinder = myBinder + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return myBinder + } - @SuppressLint("UnspecifiedImmutableFlag") override fun onCreate() { super.onCreate() - serviceJob = SupervisorJob() - downloadService = ParallelExecutor(Dispatchers.IO) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(channelId, "Downloader Service") - } - val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" } - cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT) - downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + createNotificationChannel(CHANNEL_ID, "Downloader Service") } @SuppressLint("WakelockTimeout") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) // Send a notification that service is started - Log.i(tag, "Foreground Service Started.") - startForeground(notificationId, getNotification()) + Log.i(TAG, "Foreground Service Started.") + startForeground(NOTIFICATION_ID, getNotification()) intent?.let { when (it.action) { @@ -127,7 +111,7 @@ class ForegroundService : Service(), CoroutineScope { START_STICKY } else { isServiceStarted = true - Log.i(tag, "Starting the foreground service task") + Log.i(TAG, "Starting the foreground service task") wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { @@ -142,7 +126,6 @@ class ForegroundService : Service(), CoroutineScope { * Function To Download All Tracks Available in a List **/ fun downloadAllTracks(trackList: List) { - trackList.size.also { size -> total += size isSingleDownload = (size == 1) @@ -151,8 +134,8 @@ class ForegroundService : Service(), CoroutineScope { trackList.forEach { trackStatusFlowMap[it.title] = DownloadStatus.Queued - launch { - downloadService.execute { + lifecycleScope.launch { + downloadService.value.execute { fetcher.findMp3DownloadLink(it).fold( success = { url -> enqueueDownload(url, it) @@ -170,21 +153,22 @@ class ForegroundService : Service(), CoroutineScope { private suspend fun enqueueDownload(url: String, track: TrackDetails) { // Initiating Download - addToNotification("Downloading ${track.title}") + addToNotification(Message(track.title, DownloadStatus.Downloading())) trackStatusFlowMap[track.title] = DownloadStatus.Downloading() // Enqueueing Download downloadFile(url).collect { when (it) { is DownloadResult.Error -> { - logger.d(tag) { it.message } + logger.d(TAG) { it.message } failed++ trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message)) - removeFromNotification("Downloading ${track.title}") + removeFromNotification(Message(track.title, DownloadStatus.Downloading())) } is DownloadResult.Progress -> { trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) + updateProgressInNotification(Message(track.title,DownloadStatus.Downloading(it.progress))) } is DownloadResult.Success -> { @@ -195,15 +179,15 @@ class ForegroundService : Service(), CoroutineScope { // Send Converting Status trackStatusFlowMap[track.title] = DownloadStatus.Converting - addToNotification("Processing ${track.title}") + addToNotification(Message(track.title, DownloadStatus.Converting)) // All Processing Completed for this Track job.invokeOnCompletion { converted++ trackStatusFlowMap[track.title] = DownloadStatus.Downloaded - removeFromNotification("Processing ${track.title}") + removeFromNotification(Message(track.title, DownloadStatus.Converting)) } - logger.d(tag) { "${track.title} Download Completed" } + logger.d(TAG) { "${track.title} Download Completed" } downloaded++ }.failure { error -> error.printStackTrace() @@ -211,7 +195,7 @@ class ForegroundService : Service(), CoroutineScope { failed++ trackStatusFlowMap[track.title] = DownloadStatus.Failed(error) } - removeFromNotification("Downloading ${track.title}") + removeFromNotification(Message(track.title, DownloadStatus.Downloading())) } } } @@ -219,7 +203,7 @@ class ForegroundService : Service(), CoroutineScope { } private fun releaseWakeLock() { - logger.d(tag) { "Releasing Wake Lock" } + logger.d(TAG) { "Releasing Wake Lock" } try { wakeLock?.let { if (it.isHeld) { @@ -227,21 +211,22 @@ class ForegroundService : Service(), CoroutineScope { } } } catch (e: Exception) { - logger.d(tag) { "Service stopped without being started: ${e.message}" } + logger.d(TAG) { "Service stopped without being started: ${e.message}" } } isServiceStarted = false } @Suppress("SameParameterValue") - @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(channelId: String, channelName: String) { - val channel = NotificationChannel( - channelId, - channelName, NotificationManager.IMPORTANCE_DEFAULT - ) - channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - service.createNotificationChannel(channel) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + channelName, NotificationManager.IMPORTANCE_DEFAULT + ) + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + service.createNotificationChannel(channel) + } } /* @@ -249,16 +234,18 @@ class ForegroundService : Service(), CoroutineScope { * - `Clean Up` and `Stop this Foreground Service` * */ private fun killService() { - launch { - logger.d(tag) { "Killing Self" } - messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") - downloadService.close() + lifecycleScope.launch { + logger.d(TAG) { "Killing Self" } + messageList = messageList.getEmpty().apply { + set(index = 0, Message("Cleaning And Exiting",DownloadStatus.NotDownloaded)) + } + downloadService.value.close() + downloadService.reset() updateNotification() cleanFiles(File(dir.defaultDir())) - // TODO cleanFiles(File(dir.imageCacheDir())) - messageList = mutableListOf("", "", "", "", "") + // cleanFiles(File(dir.imageCacheDir())) + messageList = messageList.getEmpty() releaseWakeLock() - serviceJob.cancel() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { stopForeground(true) stopSelf() @@ -270,6 +257,7 @@ class ForegroundService : Service(), CoroutineScope { override fun onDestroy() { super.onDestroy() + logger.i(TAG) { "onDestroy, isFinished: $isFinished" } if (isFinished) { killService() } @@ -277,6 +265,7 @@ class ForegroundService : Service(), CoroutineScope { override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) + logger.i(TAG) { "onTaskRemoved, isFinished: $isFinished" } if (isFinished) { killService() } @@ -285,30 +274,39 @@ class ForegroundService : Service(), CoroutineScope { /* * Create A New Notification with all the updated data * */ - private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run { + private fun getNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run { setSmallIcon(R.drawable.ic_download_arrow) setContentTitle("Total: $total Completed:$converted Failed:$failed") setSilent(true) +// val max +// val progress = if(total != 0) 0 else (((failed+converted).toDouble() / total.toDouble()).roundToInt()) + setProgress(total,failed+converted,false) setStyle( NotificationCompat.InboxStyle().run { - addLine(messageList[messageList.size - 1]) - addLine(messageList[messageList.size - 2]) - addLine(messageList[messageList.size - 3]) - addLine(messageList[messageList.size - 4]) - addLine(messageList[messageList.size - 5]) + addLine(messageList[messageList.size - 1].asString()) + addLine(messageList[messageList.size - 2].asString()) + addLine(messageList[messageList.size - 3].asString()) + addLine(messageList[messageList.size - 4].asString()) + addLine(messageList[messageList.size - 5].asString()) } ) addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent) build() } - private fun addToNotification(message: String) { + private fun addToNotification(message: Message) { messageList.add(message) updateNotification() } - private fun removeFromNotification(message: String) { - messageList.remove(message) + private fun removeFromNotification(message: Message) { + messageList.removeAll { it.title == message.title } + updateNotification() + } + + private fun updateProgressInNotification(message: Message) { + val index = messageList.indexOfFirst { it.title == message.title } + messageList[index] = message updateNotification() } @@ -318,6 +316,13 @@ class ForegroundService : Service(), CoroutineScope { private fun updateNotification() { val mNotificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - mNotificationManager.notify(notificationId, getNotification()) + mNotificationManager.notify(NOTIFICATION_ID, getNotification()) } + + companion object { + private const val TAG: String = "Foreground Service" + private const val CHANNEL_ID = "ForegroundDownloaderService" + private const val NOTIFICATION_ID = 101 + } + } diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt new file mode 100644 index 00000000..0e03615e --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt @@ -0,0 +1,33 @@ +package com.shabinder.spotiflyer.service + +import com.shabinder.common.models.DownloadStatus + +typealias Message = Pair + +val Message.title: String get() = first + +val Message.downloadStatus: DownloadStatus get() = second + +val Message.progress: String get() = when (downloadStatus) { + is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%" + is DownloadStatus.Converting -> "-> 100%" + is DownloadStatus.Downloaded -> "-> Done" + is DownloadStatus.Failed -> "-> Failed" + is DownloadStatus.Queued -> "-> Queued" + is DownloadStatus.NotDownloaded -> "" +} + +val emptyMessage = Message("",DownloadStatus.NotDownloaded) + +// `Progress` is not being shown because we don't get get consistent Updates from Download Fun , +// all Progress data is emitted all together from fun +fun Message.asString(): String { + val statusString = when(downloadStatus){ + is DownloadStatus.Downloading -> "Downloading" + is DownloadStatus.Converting -> "Processing" + else -> "" + } + return "$statusString $title ${""/*progress*/}".trim() +} + +fun List.getEmpty(): MutableList = MutableList(size) { emptyMessage } \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt new file mode 100644 index 00000000..a9da91b0 --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt @@ -0,0 +1,74 @@ +package com.shabinder.spotiflyer.utils.autoclear + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.shabinder.common.requireNotNull +import com.shabinder.spotiflyer.utils.autoclear.AutoClear.Companion.TRIGGER +import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleCreateAndDestroyObserver +import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleResumeAndPauseObserver +import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleStartAndStopObserver +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class AutoClear( + lifecycle: Lifecycle, + private val initializer: (() -> T)?, + private val trigger: TRIGGER = TRIGGER.ON_CREATE, +) : ReadWriteProperty { + + companion object { + enum class TRIGGER { + ON_CREATE, + ON_START, + ON_RESUME + } + } + + private var _value: T? + get() = observer.value + set(value) { observer.value = value } + + val value: T get() = _value.requireNotNull() + + private val observer: LifecycleAutoInitializer by lazy { + when(trigger) { + TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer) + TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer) + TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer) + } + } + + init { + lifecycle.addObserver(observer) + } + + override fun getValue(thisRef: LifecycleOwner, property: KProperty<*>): T { + + if (_value != null) { + return value + } + + // If for Some Reason Initializer is not invoked even after Initialisation, invoke it after checking state + if (thisRef.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + return initializer?.invoke().also { _value = it } + ?: throw IllegalStateException("The value has not yet been set or no default initializer provided") + } else { + throw IllegalStateException("Activity might have been destroyed or not initialized yet") + } + } + + override fun setValue(thisRef: LifecycleOwner, property: KProperty<*>, value: T?) { + this._value = value + } + + fun reset() { + this._value = null + } +} + +fun LifecycleOwner.autoClear( + trigger: TRIGGER = TRIGGER.ON_CREATE, + initializer: () -> T +): AutoClear { + return AutoClear(this.lifecycle, initializer, trigger) +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt new file mode 100644 index 00000000..63c385fc --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt @@ -0,0 +1,62 @@ +package com.shabinder.spotiflyer.utils.autoclear + +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class AutoClearFragment( + fragment: Fragment, + private val initializer: (() -> T)? +) : ReadWriteProperty { + + private var _value: T? = null + + init { + fragment.lifecycle.addObserver(object : DefaultLifecycleObserver { + val viewLifecycleOwnerObserver = Observer { viewLifecycleOwner -> + + viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + _value = null + } + }) + } + + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver) + } + + override fun onDestroy(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver) + } + } + ) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + val value = _value + + if (value != null) { + return value + } + + if (thisRef.viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + return initializer?.invoke().also { _value = it } + ?: throw IllegalStateException("The value has not yet been set or no default initializer provided") + } else { + throw IllegalStateException("Fragment might have been destroyed or not initialized yet") + } + } + + override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) { + _value = value + } +} + +fun Fragment.autoClear(initializer: () -> T): AutoClearFragment { + return AutoClearFragment(this, initializer) +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt new file mode 100644 index 00000000..f1475372 --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt @@ -0,0 +1,7 @@ +package com.shabinder.spotiflyer.utils.autoclear + +import androidx.lifecycle.DefaultLifecycleObserver + +interface LifecycleAutoInitializer: DefaultLifecycleObserver { + var value: T? +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt new file mode 100644 index 00000000..9219b22c --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt @@ -0,0 +1,21 @@ +package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers + +import androidx.lifecycle.LifecycleOwner +import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer + +class LifecycleCreateAndDestroyObserver( + private val initializer: (() -> T)? +) : LifecycleAutoInitializer { + + override var value: T? = null + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + value = initializer?.invoke() + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + value = null + } +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt new file mode 100644 index 00000000..b68feb7a --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt @@ -0,0 +1,21 @@ +package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers + +import androidx.lifecycle.LifecycleOwner +import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer + +class LifecycleResumeAndPauseObserver( + private val initializer: (() -> T)? +) : LifecycleAutoInitializer { + + override var value: T? = null + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + value = initializer?.invoke() + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + value = null + } +} \ No newline at end of file diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt new file mode 100644 index 00000000..d7c4d079 --- /dev/null +++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt @@ -0,0 +1,21 @@ +package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers + +import androidx.lifecycle.LifecycleOwner +import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer + +class LifecycleStartAndStopObserver( + private val initializer: (() -> T)? +) : LifecycleAutoInitializer { + + override var value: T? = null + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + value = initializer?.invoke() + } + + override fun onStop(owner: LifecycleOwner) { + super.onStop(owner) + value = null + } +} \ No newline at end of file diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 7bca1caa..280ebe5a 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -49,7 +49,7 @@ object Versions { const val minSdkVersion = 21 const val compileSdkVersion = 29 const val targetSdkVersion = 29 - const val androidLifecycle = "2.3.0" + const val androidxLifecycle = "2.3.1" } object HostOS { diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt index e8f24aea..66717056 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt @@ -1,3 +1,3 @@ package com.shabinder.common -fun T?.requireNotNull() : T = requireNotNull(this) \ No newline at end of file +fun T?.requireNotNull() : T = requireNotNull(this) \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index 1eb0ad12..a5ecd0e1 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -4,7 +4,11 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message) - data class MP3ConversionFailed(override val message: String = "MP3 Converter unreachable, probably BUSY !"): SpotiFlyerException(message) + + data class MP3ConversionFailed( + val extraInfo:String? = null, + override val message: String = "MP3 Converter unreachable, probably BUSY ! \nCAUSE:$extraInfo" + ): SpotiFlyerException(message) data class NoMatchFound( val trackName: String? = null, diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt index 70e030b0..9a476f0f 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt @@ -122,6 +122,7 @@ class FetchPlatformQueryResult( trackName = track.title, trackArtists = track.artists ).flatMapError { saavnError -> + logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" } // Saavn Failed, Lets Try Fetching Now From Youtube Music youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError -> // If Both Failed Bubble the Exception Up with both StackTraces diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt index 63845c55..14d6e9f8 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt @@ -47,16 +47,16 @@ interface AudioToMp3 { "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}" ) } catch (e: Exception) { + e.printStackTrace() if(e is ClientRequestException && e.response.status.value == 404) { // No Need to Retry, Host/Converter is Busy - throw SpotiFlyerException.MP3ConversionFailed() + throw SpotiFlyerException.MP3ConversionFailed(e.message) } // Try Using New Host/Converter convertRequest(URL, audioQuality).value.also { activeHost = it.first jobLink = it.second } - e.printStackTrace() "" } retryCount-- From 06382bcda914506c5b336aca7eaace13b3a17141 Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 23 Jun 2021 00:21:03 +0530 Subject: [PATCH 09/15] Comment Out Redundant Notification Update --- .../java/com/shabinder/spotiflyer/service/ForegroundService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index f482f4ea..b343a3b1 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -168,7 +168,7 @@ class ForegroundService : LifecycleService() { is DownloadResult.Progress -> { trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) - updateProgressInNotification(Message(track.title,DownloadStatus.Downloading(it.progress))) + // updateProgressInNotification(Message(track.title,DownloadStatus.Downloading(it.progress))) } is DownloadResult.Success -> { From bb3776af565d49254a83f2ad239fd993ad267555 Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 23 Jun 2021 00:29:51 +0530 Subject: [PATCH 10/15] Better Naming and redundant Comments Removed --- .../spotiflyer/service/ForegroundService.kt | 53 +++++++------------ 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index b343a3b1..3d57d70c 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -55,8 +55,8 @@ import java.io.File class ForegroundService : LifecycleService() { - val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) } private var downloadService: AutoClear = autoClear { ParallelExecutor(Dispatchers.IO) } + val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) } private val fetcher: FetchPlatformQueryResult by inject() private val logger: Kermit by inject() private val dir: Dir by inject() @@ -70,10 +70,10 @@ class ForegroundService : LifecycleService() { } /* Variables Holding Download State */ - private var total = 0 // Total Downloads Requested - private var converted = 0 // Total Files Converted - private var downloaded = 0 // Total Files downloaded - private var failed = 0 // Total Files failed + private var total = 0 + private var converted = 0 + private var downloaded = 0 + private var failed = 0 private val isFinished get() = converted + failed == total private var isSingleDownload = false @@ -97,7 +97,7 @@ class ForegroundService : LifecycleService() { super.onStartCommand(intent, flags, startId) // Send a notification that service is started Log.i(TAG, "Foreground Service Started.") - startForeground(NOTIFICATION_ID, getNotification()) + startForeground(NOTIFICATION_ID, createNotification()) intent?.let { when (it.action) { @@ -250,36 +250,15 @@ class ForegroundService : LifecycleService() { stopForeground(true) stopSelf() } else { - stopSelf() // System will automatically close it + stopSelf() } } } - override fun onDestroy() { - super.onDestroy() - logger.i(TAG) { "onDestroy, isFinished: $isFinished" } - if (isFinished) { - killService() - } - } - - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - logger.i(TAG) { "onTaskRemoved, isFinished: $isFinished" } - if (isFinished) { - killService() - } - } - - /* - * Create A New Notification with all the updated data - * */ - private fun getNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run { + private fun createNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run { setSmallIcon(R.drawable.ic_download_arrow) setContentTitle("Total: $total Completed:$converted Failed:$failed") setSilent(true) -// val max -// val progress = if(total != 0) 0 else (((failed+converted).toDouble() / total.toDouble()).roundToInt()) setProgress(total,failed+converted,false) setStyle( NotificationCompat.InboxStyle().run { @@ -310,13 +289,20 @@ class ForegroundService : LifecycleService() { updateNotification() } - /** - * This is the method that can be called to update the Notification - */ private fun updateNotification() { val mNotificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - mNotificationManager.notify(NOTIFICATION_ID, getNotification()) + mNotificationManager.notify(NOTIFICATION_ID, createNotification()) + } + + override fun onDestroy() { + super.onDestroy() + if (isFinished) { killService() } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + if (isFinished) { killService() } } companion object { @@ -324,5 +310,4 @@ class ForegroundService : LifecycleService() { private const val CHANNEL_ID = "ForegroundDownloaderService" private const val NOTIFICATION_ID = 101 } - } From c010916953a8d7d93e1eab9e9dbf2676ca1301ff Mon Sep 17 00:00:00 2001 From: shabinder Date: Wed, 23 Jun 2021 16:43:26 +0530 Subject: [PATCH 11/15] Preference Screen & Preference Manager (WIP) --- .../com/shabinder/spotiflyer/MainActivity.kt | 29 ++++---- build.gradle.kts | 9 ++- buildSrc/buildSrc/src/main/kotlin/Versions.kt | 4 ++ .../common/models/SpotiFlyerException.kt | 5 ++ common/dependency-injection/build.gradle.kts | 2 +- .../com/shabinder/common/di/AndroidDir.kt | 7 +- .../requests}/saavn/decryptURL.kt | 5 +- .../kotlin/com/shabinder/common/di/DI.kt | 24 +++---- .../kotlin/com/shabinder/common/di/Dir.kt | 26 +------ .../common/di/FetchPlatformQueryResult.kt | 4 +- .../com/shabinder/common/di/TokenStore.kt | 2 +- .../common/di/preference/PreferenceManager.kt | 35 ++++++++++ .../common/di/providers/GaanaProvider.kt | 2 +- .../common/di/providers/ProvidersModule.kt | 16 +++++ .../common/di/providers/SaavnProvider.kt | 4 +- .../common/di/providers/SpotifyProvider.kt | 4 +- .../common/di/providers/YoutubeMp3.kt | 2 +- .../common/di/providers/YoutubeMusic.kt | 2 +- .../requests}/audioToMp3/AudioToMp3.kt | 2 +- .../requests}/gaana/GaanaRequests.kt | 2 +- .../requests}/saavn/JioSaavnRequests.kt | 4 +- .../requests}/saavn/JioSaavnUtils.kt | 4 +- .../requests}/spotify/SpotifyAuth.kt | 2 +- .../requests}/spotify/SpotifyRequests.kt | 2 +- .../requests}/youtubeMp3/Yt1sMp3.kt | 2 +- .../common/di/{saavn => utils}/JsonUtils.kt | 2 +- .../com/shabinder/common/di/DesktopActual.kt | 67 ++++++++++--------- .../com/shabinder/common/di/DesktopDir.kt | 7 +- .../requests}/saavn/decryptURL.kt | 5 +- .../kotlin/com.shabinder.common.di/IOSDir.kt | 7 +- .../saavn/decryptURL.kt | 0 .../com/shabinder/common/di/WebActual.kt | 41 ++++++------ .../kotlin/com/shabinder/common/di/WebDir.kt | 7 +- .../requests}/saavn/decryptURL.kt | 2 +- .../shabinder/common/list/SpotiFlyerList.kt | 2 + .../list/integration/SpotiFlyerListImpl.kt | 4 +- .../list/store/SpotiFlyerListStoreProvider.kt | 5 +- .../shabinder/common/main/SpotiFlyerMain.kt | 2 + .../main/integration/SpotiFlyerMainImpl.kt | 6 +- .../main/store/SpotiFlyerMainStoreProvider.kt | 8 +-- .../shabinder/common/root/SpotiFlyerRoot.kt | 8 ++- .../root/integration/SpotiFlyerRootImpl.kt | 15 ++--- desktop/src/jvmMain/kotlin/Main.kt | 30 ++++++--- settings.gradle.kts | 1 + web-app/src/main/kotlin/App.kt | 10 +-- web-app/src/main/kotlin/client.kt | 5 +- 46 files changed, 249 insertions(+), 185 deletions(-) rename common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/{ => providers/requests}/saavn/decryptURL.kt (87%) create mode 100644 common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt create mode 100644 common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/audioToMp3/AudioToMp3.kt (98%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/gaana/GaanaRequests.kt (98%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/saavn/JioSaavnRequests.kt (98%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/saavn/JioSaavnUtils.kt (70%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/spotify/SpotifyAuth.kt (97%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/spotify/SpotifyRequests.kt (97%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers/requests}/youtubeMp3/Yt1sMp3.kt (97%) rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{saavn => utils}/JsonUtils.kt (98%) rename common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/{ => providers/requests}/saavn/decryptURL.kt (86%) rename common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/{ => providers.requests}/saavn/decryptURL.kt (100%) rename common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/{ => providers/requests}/saavn/decryptURL.kt (60%) diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 0bd21b8d..3acd9f81 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -52,6 +52,7 @@ import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsHeight import com.google.accompanist.insets.statusBarsPadding import com.shabinder.common.di.* +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.models.Actions import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformActions @@ -78,6 +79,7 @@ class MainActivity : ComponentActivity() { private val fetcher: FetchPlatformQueryResult by inject() private val dir: Dir by inject() + private val preferenceManager: PreferenceManager by inject() private lateinit var root: SpotiFlyerRoot private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks private val trackStatusFlow = MutableSharedFlow>(1) @@ -129,18 +131,18 @@ class MainActivity : ComponentActivity() { AnalyticsDialog( askForAnalyticsPermission, enableAnalytics = { - dir.toggleAnalytics(true) - dir.firstLaunchDone() + preferenceManager.toggleAnalytics(true) + preferenceManager.firstLaunchDone() }, dismissDialog = { askForAnalyticsPermission = false - dir.firstLaunchDone() + preferenceManager.firstLaunchDone() } ) LaunchedEffect(view) { permissionGranted.value = checkPermissions() - if(dir.isFirstLaunch) { + if(preferenceManager.isFirstLaunch) { delay(2500) // Ask For Analytics Permission on first Dialog askForAnalyticsPermission = true @@ -161,7 +163,7 @@ class MainActivity : ComponentActivity() { * for `Github Downloads` we will track Downloads using : https://tooomm.github.io/github-release-stats/?username=Shabinder&repository=SpotiFlyer * */ if(isGithubRelease) { checkIfLatestVersion() } - if(dir.isAnalyticsEnabled && !isGithubRelease) { + if(preferenceManager.isAnalyticsEnabled && !isGithubRelease) { // Download/App Install Event for F-Droid builds TrackHelper.track().download().with(tracker) } @@ -246,9 +248,10 @@ class MainActivity : ComponentActivity() { dependencies = object : SpotiFlyerRoot.Dependencies{ override val storeFactory = LoggingStoreFactory(DefaultStoreFactory) override val database = this@MainActivity.dir.db - override val fetchPlatformQueryResult = this@MainActivity.fetcher - override val directories: Dir = this@MainActivity.dir - override val downloadProgressReport: MutableSharedFlow> = trackStatusFlow + override val fetchQuery = this@MainActivity.fetcher + override val dir: Dir = this@MainActivity.dir + override val preferenceManager = this@MainActivity.preferenceManager + override val downloadProgressFlow: MutableSharedFlow> = trackStatusFlow override val actions = object: Actions { override val platformActions = object : PlatformActions { @@ -316,7 +319,7 @@ class MainActivity : ComponentActivity() { * */ override val analytics = object: Analytics { override fun appLaunchEvent() { - if(dir.isAnalyticsEnabled){ + if(preferenceManager.isAnalyticsEnabled){ TrackHelper.track() .event("events","App_Launch") .name("App Launch").with(tracker) @@ -324,7 +327,7 @@ class MainActivity : ComponentActivity() { } override fun homeScreenVisit() { - if(dir.isAnalyticsEnabled){ + if(preferenceManager.isAnalyticsEnabled){ // HomeScreen Visit Event TrackHelper.track().screen("/main_activity/home_screen") .title("HomeScreen").with(tracker) @@ -332,7 +335,7 @@ class MainActivity : ComponentActivity() { } override fun listScreenVisit() { - if(dir.isAnalyticsEnabled){ + if(preferenceManager.isAnalyticsEnabled){ // ListScreen Visit Event TrackHelper.track().screen("/main_activity/list_screen") .title("ListScreen").with(tracker) @@ -340,7 +343,7 @@ class MainActivity : ComponentActivity() { } override fun donationDialogVisit() { - if (dir.isAnalyticsEnabled) { + if (preferenceManager.isAnalyticsEnabled) { // Donation Dialog Open Event TrackHelper.track().screen("/main_activity/donation_dialog") .title("DonationDialog").with(tracker) @@ -384,7 +387,7 @@ class MainActivity : ComponentActivity() { val f = File(path) if (f.canWrite()) { // hell yeah :) - dir.setDownloadDirectory(path) + preferenceManager.setDownloadDirectory(path) showPopUpMessage( "Download Directory Set to:\n${dir.defaultDir()} " ) diff --git a/build.gradle.kts b/build.gradle.kts index c73376d8..c4f0b7fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,12 +33,17 @@ allprojects { tasks.withType().configureEach { kotlinOptions { jvmTarget = "1.8" - useIR = true + freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn" } } afterEvaluate { project.extensions.findByType()?.let { kmpExt -> - kmpExt.sourceSets.removeAll { it.name == "androidAndroidTestRelease" } + kmpExt.sourceSets.run { + all { + languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi") + } + removeAll { it.name == "androidAndroidTestRelease" } + } } } } diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index 280ebe5a..fbb3f1b9 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -60,6 +60,10 @@ object HostOS { val isLinux = hostOs.startsWith("Linux",true) } +object MultiPlatformSettings { + const val dep = "com.russhwolf:multiplatform-settings-no-arg:0.7.7" +} + object Koin { val core = "io.insert-koin:koin-core:${Versions.koin}" val test = "io.insert-koin:koin-test:${Versions.koin}" diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index a5ecd0e1..0374c95b 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -10,6 +10,11 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag override val message: String = "MP3 Converter unreachable, probably BUSY ! \nCAUSE:$extraInfo" ): SpotiFlyerException(message) + data class UnknownReason( + val exception: Throwable? = null, + override val message: String = "Unknown Error" + ): SpotiFlyerException(message) + data class NoMatchFound( val trackName: String? = null, override val message: String = "$trackName : NO Match Found!" diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index 2a63e0fd..6852ae09 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -32,7 +32,7 @@ kotlin { implementation(project(":common:database")) implementation("org.jetbrains.kotlinx:atomicfu:0.16.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1") - implementation("com.russhwolf:multiplatform-settings-no-arg:0.7.7") + api(MultiPlatformSettings.dep) implementation(Extras.youtubeDownloader) implementation(Extras.fuzzyWuzzy) implementation(MVIKotlin.rx) diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index 27bc641b..f6b44eee 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -22,8 +22,8 @@ import android.os.Environment import androidx.compose.ui.graphics.asImageBitmap import co.touchlab.kermit.Kermit import com.mpatric.mp3agic.Mp3File -import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods @@ -43,7 +43,7 @@ import java.net.URL @Suppress("DEPRECATION") actual class Dir actual constructor( private val logger: Kermit, - settingsPref: Settings, + private val preferenceManager: PreferenceManager, spotiFlyerDatabase: SpotiFlyerDatabase, ) { @Suppress("DEPRECATION") @@ -54,7 +54,7 @@ actual class Dir actual constructor( actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir // fun call in order to always access Updated Value - actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + + actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + File.separator + "SpotiFlyer" + File.separator actual fun isPresent(path: String): Boolean = File(path).exists() @@ -202,5 +202,4 @@ actual class Dir actual constructor( private val parallelExecutor = ParallelExecutor(Dispatchers.IO) actual val db: Database? = spotiFlyerDatabase.instance - actual val settings: Settings = settingsPref } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt similarity index 87% rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt rename to common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt index 65d67107..2a523118 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt @@ -1,8 +1,7 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.providers.requests.saavn import android.annotation.SuppressLint -import io.ktor.util.InternalAPI -import io.ktor.util.decodeBase64Bytes +import io.ktor.util.* import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.SecretKey diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index 0d23dcc0..27f30a37 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -20,13 +20,8 @@ import co.touchlab.kermit.Kermit import com.russhwolf.settings.Settings import com.shabinder.common.database.databaseModule import com.shabinder.common.database.getLogger -import com.shabinder.common.di.audioToMp3.AudioToMp3 -import com.shabinder.common.di.providers.GaanaProvider -import com.shabinder.common.di.providers.SaavnProvider -import com.shabinder.common.di.providers.SpotifyProvider -import com.shabinder.common.di.providers.YoutubeMp3 -import com.shabinder.common.di.providers.YoutubeMusic -import com.shabinder.common.di.providers.YoutubeProvider +import com.shabinder.common.di.preference.PreferenceManager +import com.shabinder.common.di.providers.providersModule import io.ktor.client.* import io.ktor.client.features.* import io.ktor.client.features.json.* @@ -42,7 +37,11 @@ import kotlin.native.concurrent.ThreadLocal fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) = startKoin { appDeclaration() - modules(commonModule(enableNetworkLogs = enableNetworkLogs), databaseModule()) + modules( + commonModule(enableNetworkLogs = enableNetworkLogs), + providersModule(), + databaseModule() + ) } // Called by IOS @@ -52,16 +51,9 @@ fun commonModule(enableNetworkLogs: Boolean) = module { single { createHttpClient(enableNetworkLogs = enableNetworkLogs) } single { Dir(get(), get(), get()) } single { Settings() } + single { PreferenceManager(get()) } single { Kermit(getLogger()) } single { TokenStore(get(), get()) } - single { AudioToMp3(get(), get()) } - single { SpotifyProvider(get(), get(), get()) } - single { GaanaProvider(get(), get(), get()) } - single { SaavnProvider(get(), get(), get(), get()) } - single { YoutubeProvider(get(), get(), get()) } - single { YoutubeMp3(get(), get()) } - single { YoutubeMusic(get(), get(), get(), get(), get()) } - single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) } } @ThreadLocal diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index 7820eb92..ee877010 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -17,8 +17,8 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit -import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.TrackDetails @@ -30,18 +30,12 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlin.math.roundToInt -const val DirKey = "downloadDir" -const val AnalyticsKey = "analytics" -const val FirstLaunch = "firstLaunch" -const val DonationInterval = "donationInterval" - expect class Dir( logger: Kermit, - settingsPref: Settings, + preferenceManager: PreferenceManager, spotiFlyerDatabase: SpotiFlyerDatabase, ) { val db: Database? - val settings: Settings fun isPresent(path: String): Boolean fun fileSeparator(): String fun defaultDir(): String @@ -54,22 +48,6 @@ expect class Dir( fun addToLibrary(path: String) } -val Dir.isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false -fun Dir.toggleAnalytics(enabled: Boolean) = settings.putBoolean(AnalyticsKey, enabled) - -fun Dir.setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath) - -val Dir.getDonationOffset: Int get() = (settings.getIntOrNull(DonationInterval) ?: 3).also { - // Min. Donation Asking Interval is `3` - if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1) -} -fun Dir.setDonationOffset(offset: Int = 5) = settings.putInt(DonationInterval, offset) - -val Dir.isFirstLaunch get() = settings.getBooleanOrNull(FirstLaunch) ?: true -fun Dir.firstLaunchDone() { - settings.putBoolean(FirstLaunch, false) -} - /* * Call this function at startup! * */ diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt index 9a476f0f..b7eabb08 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt @@ -18,7 +18,6 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit import com.shabinder.common.database.DownloadRecordDatabaseQueries -import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.SaavnProvider import com.shabinder.common.di.providers.SpotifyProvider @@ -26,6 +25,7 @@ import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeProvider import com.shabinder.common.di.providers.get +import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3 import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails @@ -35,6 +35,7 @@ import com.shabinder.common.models.event.coroutines.flatMapError import com.shabinder.common.models.event.coroutines.success import com.shabinder.common.models.spotify.Source import com.shabinder.common.requireNotNull +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -137,6 +138,7 @@ class FetchPlatformQueryResult( } } + @OptIn(DelicateCoroutinesApi::class) private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) { GlobalScope.launch(dispatcherIO) { db?.add( diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt index 041fb9a3..0526e306 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt @@ -18,7 +18,7 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit import com.shabinder.common.database.TokenDBQueries -import com.shabinder.common.di.spotify.authenticateSpotify +import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify import com.shabinder.common.models.spotify.TokenData import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt new file mode 100644 index 00000000..3e44ba91 --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt @@ -0,0 +1,35 @@ +package com.shabinder.common.di.preference + +import com.russhwolf.settings.Settings + +class PreferenceManager(settings: Settings): Settings by settings { + + companion object { + const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + const val FirstLaunch = "firstLaunch" + const val DonationInterval = "donationInterval" + } + + /* ANALYTICS */ + val isAnalyticsEnabled get() = getBooleanOrNull(AnalyticsKey) ?: false + fun toggleAnalytics(enabled: Boolean) = putBoolean(AnalyticsKey, enabled) + + + /* DOWNLOAD DIRECTORY */ + val downloadDir get() = getStringOrNull(DirKey) + fun setDownloadDirectory(newBasePath: String) = putString(DirKey, newBasePath) + + + /* OFFSET FOR WHEN TO ASK FOR SUPPORT */ + val getDonationOffset: Int get() = (getIntOrNull(DonationInterval) ?: 3).also { + // Min. Donation Asking Interval is `3` + if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1) + } + fun setDonationOffset(offset: Int = 5) = putInt(DonationInterval, offset) + + + /* TO CHECK IF THIS IS APP's FIRST LAUNCH */ + val isFirstLaunch get() = getBooleanOrNull(FirstLaunch) ?: true + fun firstLaunchDone() = putBoolean(FirstLaunch, false) +} \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt index 5fdfda90..37d27c87 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt @@ -19,7 +19,7 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit import com.shabinder.common.di.Dir import com.shabinder.common.di.finalOutputDir -import com.shabinder.common.di.gaana.GaanaRequests +import com.shabinder.common.di.providers.requests.gaana.GaanaRequests import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.SpotiFlyerException diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt new file mode 100644 index 00000000..b4d46549 --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt @@ -0,0 +1,16 @@ +package com.shabinder.common.di.providers + +import com.shabinder.common.di.FetchPlatformQueryResult +import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3 +import org.koin.dsl.module + +fun providersModule() = module { + single { AudioToMp3(get(), get()) } + single { SpotifyProvider(get(), get(), get()) } + single { GaanaProvider(get(), get(), get()) } + single { SaavnProvider(get(), get(), get(), get()) } + single { YoutubeProvider(get(), get(), get()) } + single { YoutubeMp3(get(), get()) } + single { YoutubeMusic(get(), get(), get(), get(), get()) } + single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) } +} \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt index b062f4cf..7188ada4 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt @@ -2,9 +2,9 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit import com.shabinder.common.di.Dir -import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.finalOutputDir -import com.shabinder.common.di.saavn.JioSaavnRequests +import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3 +import com.shabinder.common.di.providers.requests.saavn.JioSaavnRequests import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index b6796258..0d446611 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -22,8 +22,8 @@ import com.shabinder.common.di.TokenStore import com.shabinder.common.di.createHttpClient import com.shabinder.common.di.finalOutputDir import com.shabinder.common.di.globalJson -import com.shabinder.common.di.spotify.SpotifyRequests -import com.shabinder.common.di.spotify.authenticateSpotify +import com.shabinder.common.di.providers.requests.spotify.SpotifyRequests +import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.NativeAtomicReference import com.shabinder.common.models.PlatformQueryResult diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt index 03d0c758..5ba98a5c 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt @@ -17,7 +17,7 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit -import com.shabinder.common.di.youtubeMp3.Yt1sMp3 +import com.shabinder.common.di.providers.requests.youtubeMp3.Yt1sMp3 import com.shabinder.common.models.corsApi import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.map diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index ec68f6bc..0949226e 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -17,7 +17,7 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit -import com.shabinder.common.di.audioToMp3.AudioToMp3 +import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3 import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.YoutubeTrack diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt similarity index 98% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt index 14d6e9f8..6e4f4261 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt @@ -1,4 +1,4 @@ -package com.shabinder.common.di.audioToMp3 +package com.shabinder.common.di.providers.requests.audioToMp3 import co.touchlab.kermit.Kermit import com.shabinder.common.models.AudioQuality diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt similarity index 98% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt index 11f36a52..373945ae 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt @@ -14,7 +14,7 @@ * * along with this program. If not, see . */ -package com.shabinder.common.di.gaana +package com.shabinder.common.di.providers.requests.gaana import com.shabinder.common.models.corsApi import com.shabinder.common.models.gaana.GaanaAlbum diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt similarity index 98% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt index 79c6188c..b3b59ecf 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt @@ -1,8 +1,8 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.providers.requests.saavn import co.touchlab.kermit.Kermit -import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.globalJson +import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3 import com.shabinder.common.models.corsApi import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.map diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt similarity index 70% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt index d9e38f2d..4ff3e563 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt @@ -1,4 +1,6 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.providers.requests.saavn + +import com.shabinder.common.di.utils.unescape expect suspend fun decryptURL(url: String): String diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt similarity index 97% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt index 33d24a0d..4bb67403 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt @@ -14,7 +14,7 @@ * * along with this program. If not, see . */ -package com.shabinder.common.di.spotify +package com.shabinder.common.di.providers.requests.spotify import com.shabinder.common.di.globalJson import com.shabinder.common.models.SpotiFlyerException diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt similarity index 97% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt index 22acff05..7e31a4f6 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt @@ -14,7 +14,7 @@ * * along with this program. If not, see . */ -package com.shabinder.common.di.spotify +package com.shabinder.common.di.providers.requests.spotify import com.shabinder.common.models.NativeAtomicReference import com.shabinder.common.models.corsApi diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt similarity index 97% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt index 5bac8eff..3e7feff2 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt @@ -14,7 +14,7 @@ * * along with this program. If not, see . */ -package com.shabinder.common.di.youtubeMp3 +package com.shabinder.common.di.providers.requests.youtubeMp3 import co.touchlab.kermit.Kermit import com.shabinder.common.models.corsApi diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt similarity index 98% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt index 367f39a1..3e06f0b5 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt @@ -1,4 +1,4 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.utils /* * JSON UTILS diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index cbc449d7..4e9cda1e 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -19,6 +19,7 @@ package com.shabinder.common.di import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -40,41 +41,43 @@ actual suspend fun downloadTracks( ) { list.forEach { trackDetails -> DownloadScope.execute { // Send Download to Pool. - val url = fetcher.findMp3DownloadLink(trackDetails) - if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL - downloadFile(url).collect { - when (it) { - is DownloadResult.Error -> { - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } - ) - } - is DownloadResult.Progress -> { - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) } - ) - } - is DownloadResult.Success -> { // Todo clear map - dir.saveFileWithMetadata(it.byteArray, trackDetails) {} - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } - ) + fetcher.findMp3DownloadLink(trackDetails).fold( + success = { url -> + downloadFile(url).collect { + when (it) { + is DownloadResult.Error -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause))) } + ) + } + is DownloadResult.Progress -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) } + ) + } + is DownloadResult.Success -> { // Todo clear map + dir.saveFileWithMetadata(it.byteArray, trackDetails) {} + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } + ) + } } } + }, + failure = { error -> + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(error)) } + ) } - } else { - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } - ) - } + ) } } } diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt index ae393df9..efff1f9a 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import co.touchlab.kermit.Kermit import com.mpatric.mp3agic.Mp3File -import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.models.TrackDetails import com.shabinder.database.Database import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ import javax.imageio.ImageIO actual class Dir actual constructor( private val logger: Kermit, - settingsPref: Settings, + private val preferenceManager: PreferenceManager, spotiFlyerDatabase: SpotiFlyerDatabase, ) { @@ -55,7 +55,7 @@ actual class Dir actual constructor( private val defaultBaseDir = System.getProperty("user.home") - actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() + + actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() + "SpotiFlyer" + fileSeparator() actual fun isPresent(path: String): Boolean = File(path).exists() @@ -199,7 +199,6 @@ actual class Dir actual constructor( } actual val db: Database? = spotiFlyerDatabase.instance - actual val settings: Settings = settingsPref } fun BufferedImage.toImageBitmap() = Image.makeFromEncoded( diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt similarity index 86% rename from common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt rename to common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt index 80153b91..e77b3045 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt @@ -1,7 +1,6 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.providers.requests.saavn -import io.ktor.util.InternalAPI -import io.ktor.util.decodeBase64Bytes +import io.ktor.util.* import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.SecretKey diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt index 10dae478..5ea6efaa 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt @@ -1,8 +1,8 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit -import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.models.TrackDetails import com.shabinder.database.Database import kotlinx.coroutines.GlobalScope @@ -24,7 +24,7 @@ import platform.UIKit.UIImageJPEGRepresentation actual class Dir actual constructor( val logger: Kermit, - settingsPref: Settings, + private val preferenceManager: PreferenceManager, spotiFlyerDatabase: SpotiFlyerDatabase, ) { @@ -35,7 +35,7 @@ actual class Dir actual constructor( private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!.path!! // TODO Error Handling - actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + + actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() + "SpotiFlyer" + fileSeparator() private val defaultDirURL: NSURL by lazy { @@ -176,6 +176,5 @@ actual class Dir actual constructor( // TODO } - actual val settings: Settings = settingsPref actual val db: Database? = spotiFlyerDatabase.instance } diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/providers.requests/saavn/decryptURL.kt similarity index 100% rename from common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt rename to common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/providers.requests/saavn/decryptURL.kt diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt index 5c0740a1..0682cc22 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt @@ -18,6 +18,7 @@ package com.shabinder.common.di import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.models.SpotiFlyerException import com.shabinder.common.models.TrackDetails import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -41,29 +42,31 @@ actual suspend fun downloadTracks( list.forEach { track -> withContext(dispatcherIO) { allTracksStatus[track.title] = DownloadStatus.Queued - val url = fetcher.findMp3DownloadLink(track) - if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL - downloadFile(url).collect { - when (it) { - is DownloadResult.Success -> { - println("Download Completed") - dir.saveFileWithMetadata(it.byteArray, track) {} - } - is DownloadResult.Error -> { - allTracksStatus[track.title] = DownloadStatus.Failed - println("Download Error: ${track.title}") - } - is DownloadResult.Progress -> { - allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress) - println("Download Progress: ${it.progress} : ${track.title}") + fetcher.findMp3DownloadLink(track).fold( + success = { url -> + downloadFile(url).collect { + when (it) { + is DownloadResult.Success -> { + println("Download Completed") + dir.saveFileWithMetadata(it.byteArray, track) {} + } + is DownloadResult.Error -> { + allTracksStatus[track.title] = DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause)) + println("Download Error: ${track.title}") + } + is DownloadResult.Progress -> { + allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress) + println("Download Progress: ${it.progress} : ${track.title}") + } } + DownloadProgressFlow.emit(allTracksStatus) } + }, + failure = { error -> + allTracksStatus[track.title] = DownloadStatus.Failed(error) DownloadProgressFlow.emit(allTracksStatus) } - } else { - allTracksStatus[track.title] = DownloadStatus.Failed - DownloadProgressFlow.emit(allTracksStatus) - } + ) } } } diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt index c909d10e..cc18e5d2 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt @@ -17,13 +17,13 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit -import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase -import com.shabinder.common.di.gaana.corsApi +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.corsApi import com.shabinder.database.Database import kotlinext.js.Object import kotlinext.js.js @@ -34,7 +34,7 @@ import org.w3c.dom.ImageBitmap actual class Dir actual constructor( private val logger: Kermit, - settingsPref: Settings, + private val preferenceManager: PreferenceManager, spotiFlyerDatabase: SpotiFlyerDatabase, ) { /*init { @@ -116,7 +116,6 @@ actual class Dir actual constructor( private suspend fun freshImage(url: String): ImageBitmap? = null actual val db: Database? = spotiFlyerDatabase.instance - actual val settings: Settings = settingsPref } fun ByteArray.toArrayBuffer(): ArrayBuffer { diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt similarity index 60% rename from common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt rename to common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt index e5264bbb..540525fe 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt @@ -1,4 +1,4 @@ -package com.shabinder.common.di.saavn +package com.shabinder.common.di.providers.requests.saavn actual suspend fun decryptURL(url: String): String { TODO("Not yet implemented") diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index 1503b3d7..d2feb248 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.Picture +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.list.integration.SpotiFlyerListImpl import com.shabinder.common.models.Consumer import com.shabinder.common.models.DownloadStatus @@ -67,6 +68,7 @@ interface SpotiFlyerList { val storeFactory: StoreFactory val fetchQuery: FetchPlatformQueryResult val dir: Dir + val preferenceManager: PreferenceManager val link: String val listOutput: Consumer val downloadProgressFlow: MutableSharedFlow> diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index ede4efee..0b4a38d7 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -22,7 +22,6 @@ import com.arkivanov.decompose.lifecycle.doOnResume import com.arkivanov.decompose.value.Value import com.shabinder.common.caching.Cache import com.shabinder.common.di.Picture -import com.shabinder.common.di.setDonationOffset import com.shabinder.common.di.utils.asValue import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList.Dependencies @@ -48,6 +47,7 @@ internal class SpotiFlyerListImpl( instanceKeeper.getStore { SpotiFlyerListStoreProvider( dir = this.dir, + preferenceManager = preferenceManager, storeFactory = storeFactory, fetchQuery = fetchQuery, downloadProgressFlow = downloadProgressFlow, @@ -79,7 +79,7 @@ internal class SpotiFlyerListImpl( } override fun snoozeDonationDialog() { - dir.setDonationOffset(offset = 10) + preferenceManager.setDonationOffset(offset = 10) } override suspend fun loadImage(url: String, isCover: Boolean): Picture { diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt index a2f17aa9..fa71554d 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/store/SpotiFlyerListStoreProvider.kt @@ -24,7 +24,7 @@ import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.downloadTracks -import com.shabinder.common.di.getDonationOffset +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.store.SpotiFlyerListStore.Intent import com.shabinder.common.models.DownloadStatus @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.collect internal class SpotiFlyerListStoreProvider( private val dir: Dir, + private val preferenceManager: PreferenceManager, private val storeFactory: StoreFactory, private val fetchQuery: FetchPlatformQueryResult, private val link: String, @@ -68,7 +69,7 @@ internal class SpotiFlyerListStoreProvider( dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also { // See if It's Time we can request for support for maintaining this project or not fetchQuery.logger.d(message = { "Database List Last ID: $it" }, tag = "Database Last ID") - val offset = dir.getDonationOffset + val offset = preferenceManager.getDonationOffset dispatch( Result.AskForSupport( // Every 3rd Interval or After some offset diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 275972ab..9bfc18ec 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -21,6 +21,7 @@ import com.arkivanov.decompose.value.Value import com.arkivanov.mvikotlin.core.store.StoreFactory import com.shabinder.common.di.Dir import com.shabinder.common.di.Picture +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.main.integration.SpotiFlyerMainImpl import com.shabinder.common.models.Consumer import com.shabinder.common.models.DownloadRecord @@ -63,6 +64,7 @@ interface SpotiFlyerMain { val storeFactory: StoreFactory val database: Database? val dir: Dir + val preferenceManager: PreferenceManager val mainAnalytics: Analytics } diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index d44878e7..5872d0c8 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -23,7 +23,10 @@ import com.shabinder.common.caching.Cache import com.shabinder.common.di.Picture import com.shabinder.common.di.utils.asValue import com.shabinder.common.main.SpotiFlyerMain -import com.shabinder.common.main.SpotiFlyerMain.* +import com.shabinder.common.main.SpotiFlyerMain.Dependencies +import com.shabinder.common.main.SpotiFlyerMain.HomeCategory +import com.shabinder.common.main.SpotiFlyerMain.Output +import com.shabinder.common.main.SpotiFlyerMain.State import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider import com.shabinder.common.main.store.getStore @@ -41,6 +44,7 @@ internal class SpotiFlyerMainImpl( private val store = instanceKeeper.getStore { SpotiFlyerMainStoreProvider( + preferenceManager = preferenceManager, storeFactory = storeFactory, database = database, dir = dir diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt index 9bc41d10..4ee7c951 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/store/SpotiFlyerMainStoreProvider.kt @@ -22,8 +22,7 @@ import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.shabinder.common.di.Dir -import com.shabinder.common.di.isAnalyticsEnabled -import com.shabinder.common.di.toggleAnalytics +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain.State import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent @@ -39,6 +38,7 @@ import kotlinx.coroutines.flow.map internal class SpotiFlyerMainStoreProvider( private val storeFactory: StoreFactory, + private val preferenceManager: PreferenceManager, private val dir: Dir, database: Database? ) { @@ -76,7 +76,7 @@ internal class SpotiFlyerMainStoreProvider( private inner class ExecutorImpl : SuspendExecutor() { override suspend fun executeAction(action: Unit, getState: () -> State) { - dispatch(Result.ToggleAnalytics(dir.isAnalyticsEnabled)) + dispatch(Result.ToggleAnalytics(preferenceManager.isAnalyticsEnabled)) updates?.collect { dispatch(Result.ItemsLoaded(it)) } @@ -91,7 +91,7 @@ internal class SpotiFlyerMainStoreProvider( is Intent.SelectCategory -> dispatch(Result.CategoryChanged(intent.category)) is Intent.ToggleAnalytics -> { dispatch(Result.ToggleAnalytics(intent.enabled)) - dir.toggleAnalytics(intent.enabled) + preferenceManager.toggleAnalytics(intent.enabled) } } } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index c266f18f..9c260c97 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -22,6 +22,7 @@ import com.arkivanov.decompose.value.Value import com.arkivanov.mvikotlin.core.store.StoreFactory import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.models.Actions @@ -49,9 +50,10 @@ interface SpotiFlyerRoot { interface Dependencies { val storeFactory: StoreFactory val database: Database? - val fetchPlatformQueryResult: FetchPlatformQueryResult - val directories: Dir - val downloadProgressReport: MutableSharedFlow> + val fetchQuery: FetchPlatformQueryResult + val dir: Dir + val preferenceManager: PreferenceManager + val downloadProgressFlow: MutableSharedFlow> val actions: Actions val analytics: Analytics } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index d2b5fb72..96ea55ac 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -27,7 +27,6 @@ import com.arkivanov.decompose.router import com.arkivanov.decompose.statekeeper.Parcelable import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.value.Value -import com.shabinder.common.di.Dir import com.shabinder.common.di.dispatcherIO import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.main.SpotiFlyerMain @@ -39,6 +38,7 @@ import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Dependencies import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -77,7 +77,7 @@ internal class SpotiFlyerRootImpl( instanceKeeper.ensureNeverFrozen() methods.value = dependencies.actions.freeze() /*Init App Launch & Authenticate Spotify Client*/ - initAppLaunchAndAuthenticateSpotify(dependencies.fetchPlatformQueryResult::authenticateSpotifyClient) + initAppLaunchAndAuthenticateSpotify(dependencies.fetchQuery::authenticateSpotifyClient) } private val router = @@ -128,6 +128,7 @@ internal class SpotiFlyerRootImpl( } } + @OptIn(DelicateCoroutinesApi::class) private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) { GlobalScope.launch(dispatcherIO) { analytics.appLaunchEvent() @@ -150,10 +151,7 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer< componentContext = componentContext, dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies { override val mainOutput: Consumer = output - override val dir: Dir = directories - override val mainAnalytics = object : SpotiFlyerMain.Analytics { - override fun donationDialogVisit() = analytics.donationDialogVisit() - } + override val mainAnalytics = object : SpotiFlyerMain.Analytics , Analytics by analytics {} } ) @@ -161,11 +159,8 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out SpotiFlyerList( componentContext = componentContext, dependencies = object : SpotiFlyerList.Dependencies, Dependencies by dependencies { - override val fetchQuery = fetchPlatformQueryResult - override val dir: Dir = directories override val link: String = link override val listOutput: Consumer = output - override val downloadProgressFlow = downloadProgressReport - override val listAnalytics = object : SpotiFlyerList.Analytics {} + override val listAnalytics = object : SpotiFlyerList.Analytics, Analytics by analytics {} } ) diff --git a/desktop/src/jvmMain/kotlin/Main.kt b/desktop/src/jvmMain/kotlin/Main.kt index fb3045f9..684b635b 100644 --- a/desktop/src/jvmMain/kotlin/Main.kt +++ b/desktop/src/jvmMain/kotlin/Main.kt @@ -27,12 +27,21 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen import com.arkivanov.mvikotlin.core.lifecycle.LifecycleRegistry import com.arkivanov.mvikotlin.core.lifecycle.resume import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory -import com.shabinder.common.di.* +import com.shabinder.common.di.Dir +import com.shabinder.common.di.DownloadProgressFlow +import com.shabinder.common.di.FetchPlatformQueryResult +import com.shabinder.common.di.initKoin +import com.shabinder.common.di.isInternetAccessible +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.models.Actions import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.TrackDetails import com.shabinder.common.root.SpotiFlyerRoot -import com.shabinder.common.uikit.* +import com.shabinder.common.uikit.SpotiFlyerColors +import com.shabinder.common.uikit.SpotiFlyerRootContent +import com.shabinder.common.uikit.SpotiFlyerShapes +import com.shabinder.common.uikit.SpotiFlyerTypography +import com.shabinder.common.uikit.colorOffWhite import com.shabinder.database.Database import kotlinx.coroutines.runBlocking import org.piwik.java.tracking.PiwikTracker @@ -79,10 +88,11 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot = componentContext = componentContext, dependencies = object : SpotiFlyerRoot.Dependencies { override val storeFactory = DefaultStoreFactory - override val fetchPlatformQueryResult: FetchPlatformQueryResult = koin.get() - override val directories: Dir = koin.get() - override val database: Database? = directories.db - override val downloadProgressReport = DownloadProgressFlow + override val fetchQuery: FetchPlatformQueryResult = koin.get() + override val dir: Dir = koin.get() + override val database: Database? = dir.db + override val preferenceManager: PreferenceManager = koin.get() + override val downloadProgressFlow = DownloadProgressFlow override val actions: Actions = object: Actions { override val platformActions = object : PlatformActions {} @@ -100,7 +110,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot = APPROVE_OPTION -> { val directory = fileChooser.selectedFile if(directory.canWrite()){ - directories.setDownloadDirectory(directory.absolutePath) + preferenceManager.setDownloadDirectory(directory.absolutePath) showPopUpMessage("Set New Download Directory:\n${directory.absolutePath}") } else { showPopUpMessage("Cant Write to Selected Directory!") @@ -137,10 +147,10 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot = } override val analytics = object: SpotiFlyerRoot.Analytics { override fun appLaunchEvent() { - if(directories.isFirstLaunch) { + if(preferenceManager.isFirstLaunch) { // Enable Analytics on First Launch - directories.toggleAnalytics(true) - directories.firstLaunchDone() + preferenceManager.toggleAnalytics(true) + preferenceManager.firstLaunchDone() } tracker.trackAsync { eventName = "App Launch" diff --git a/settings.gradle.kts b/settings.gradle.kts index 3748628b..dcde5caa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,7 @@ include( ":common:root", ":common:main", ":common:list", + ":common:preference", ":common:data-models", ":common:dependency-injection", ":android", diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt index 03d1ed52..785c1785 100644 --- a/web-app/src/main/kotlin/App.kt +++ b/web-app/src/main/kotlin/App.kt @@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.shabinder.common.di.DownloadProgressFlow +import com.shabinder.common.di.preference.PreferenceManager import com.shabinder.common.models.Actions import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.TrackDetails @@ -58,10 +59,11 @@ class App(props: AppProps): RComponent(props) { private val root = SpotiFlyerRoot(ctx, object : SpotiFlyerRoot.Dependencies { override val storeFactory: StoreFactory = LoggingStoreFactory(DefaultStoreFactory) - override val fetchPlatformQueryResult = dependencies.fetchPlatformQueryResult - override val directories = dependencies.directories - override val database: Database? = directories.db - override val downloadProgressReport = DownloadProgressFlow + override val fetchQuery = dependencies.fetchPlatformQueryResult + override val dir = dependencies.directories + override val preferenceManager: PreferenceManager = dependencies.preferenceManager + override val database: Database? = dir.db + override val downloadProgressFlow = DownloadProgressFlow override val actions = object : Actions { override val platformActions = object : PlatformActions {} diff --git a/web-app/src/main/kotlin/client.kt b/web-app/src/main/kotlin/client.kt index 996700ff..36661535 100644 --- a/web-app/src/main/kotlin/client.kt +++ b/web-app/src/main/kotlin/client.kt @@ -18,11 +18,12 @@ import co.touchlab.kermit.Kermit import com.shabinder.common.di.Dir import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.initKoin -import react.dom.render +import com.shabinder.common.di.preference.PreferenceManager import kotlinx.browser.document import kotlinx.browser.window import org.koin.core.component.KoinComponent import org.koin.core.component.get +import react.dom.render fun main() { window.onload = { @@ -38,10 +39,12 @@ object AppDependencies : KoinComponent { val logger: Kermit val directories: Dir val fetchPlatformQueryResult: FetchPlatformQueryResult + val preferenceManager: PreferenceManager init { initKoin() directories = get() logger = get() fetchPlatformQueryResult = get() + preferenceManager = get() } } \ No newline at end of file From 08aa6d0f18e5630a72513c61b3d02f421da2bc55 Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 25 Jun 2021 12:36:29 +0530 Subject: [PATCH 12/15] Preference Screen (WIP) --- .../shabinder/common/uikit/DonationDialog.kt | 4 +- .../com/shabinder/common/uikit/Color.kt | 1 + .../common/uikit/SpotiFlyerListUi.kt | 22 ++--- .../common/uikit/SpotiFlyerMainUi.kt | 81 ++++++++++++------- .../common/uikit/SpotiFlyerRootUi.kt | 4 +- .../common/uikit/dialogs/Donation.kt | 32 ++++++++ .../shabinder/common/list/SpotiFlyerList.kt | 2 +- .../list/integration/SpotiFlyerListImpl.kt | 2 +- .../shabinder/common/main/SpotiFlyerMain.kt | 2 + .../main/integration/SpotiFlyerMainImpl.kt | 4 + common/preference/build.gradle.kts | 35 ++++++++ .../src/androidMain/AndroidManifest.xml | 18 +++++ .../common/preference/SpotiFlyerPreference.kt | 63 +++++++++++++++ .../integration/SpotiFlyerPreferenceImpl.kt | 71 ++++++++++++++++ .../preference/store/InstanceKeeperExt.kt | 37 +++++++++ .../store/SpotiFlyerPreferenceStore.kt | 29 +++++++ .../SpotiFlyerPreferenceStoreProvider.kt | 73 +++++++++++++++++ 17 files changed, 431 insertions(+), 49 deletions(-) create mode 100644 common/preference/build.gradle.kts create mode 100644 common/preference/src/androidMain/AndroidManifest.xml create mode 100644 common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt create mode 100644 common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt create mode 100644 common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/InstanceKeeperExt.kt create mode 100644 common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt create mode 100644 common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStoreProvider.kt diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt index 71eeb0a2..5ad20017 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt @@ -126,10 +126,10 @@ actual fun DonationDialog( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth() ) { - OutlinedButton(onClick = onSnooze) { + OutlinedButton(onClick = onDismiss) { Text("Dismiss.") } - TextButton(onClick = onDismiss, colors = ButtonDefaults.buttonColors()) { + TextButton(onClick = onSnooze, colors = ButtonDefaults.buttonColors()) { Text("Remind Later!") } } diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt index 0679396c..edef2cb4 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Color val colorPrimary = Color(0xFFFC5C7D) val colorPrimaryDark = Color(0xFFCE1CFF) val colorAccent = Color(0xFF9AB3FF) +val colorAccentVariant = Color(0xFF3457D5) val colorRedError = Color(0xFFFF9494) val colorSuccessGreen = Color(0xFF59C351) val darkBackgroundColor = Color(0xFF000000) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt index 72dce041..8df74b56 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt @@ -40,9 +40,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -57,6 +54,7 @@ import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods +import com.shabinder.common.uikit.dialogs.DonationDialogComponent @OptIn(ExperimentalMaterialApi::class) @Composable @@ -104,25 +102,19 @@ fun SpotiFlyerListContent( state = listState, modifier = Modifier.fillMaxSize(), ) + // Donation Dialog Visibility - var visibilty by remember { mutableStateOf(false) } - DonationDialog( - isVisible = visibilty, - onDismiss = { - visibilty = false - }, - onSnooze = { - visibilty = false - component.snoozeDonationDialog() - } - ) + val (openDonationDialog,dismissDonationDialog,snoozeDonationDialog) = DonationDialogComponent { + component.dismissDonationDialogSetOffset() + } + DownloadAllButton( onClick = { component.onDownloadAllClicked(model.trackList) // Check If we are allowed to show donation Dialog if (model.askForDonation) { // Show Donation Dialog - visibilty = true + openDonationDialog() } }, modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt index d8279536..8fe52a33 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt @@ -17,21 +17,54 @@ package com.shabinder.common.uikit import androidx.compose.animation.Crossfade -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Switch +import androidx.compose.material.SwitchDefaults +import androidx.compose.material.Tab +import androidx.compose.material.TabPosition +import androidx.compose.material.TabRow import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material.Text +import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults.textFieldColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.History import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.rounded.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.rounded.CardGiftcard +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Flag +import androidx.compose.material.icons.rounded.Insights +import androidx.compose.material.icons.rounded.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -50,11 +83,16 @@ import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain.HomeCategory import com.shabinder.common.models.DownloadRecord import com.shabinder.common.models.methods +import com.shabinder.common.uikit.dialogs.DonationDialogComponent @Composable fun SpotiFlyerMainContent(component: SpotiFlyerMain) { val model by component.model.subscribeAsState() + val (openDonationDialog,_,_) = DonationDialogComponent { + component.dismissDonationDialogOffset() + } + Column { SearchPanel( model.link, @@ -65,14 +103,17 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain) { HomeTabBar( model.selectedCategory, HomeCategory.values(), - component::selectCategory + component::selectCategory, ) when (model.selectedCategory) { HomeCategory.About -> AboutColumn( analyticsEnabled = model.isAnalyticsEnabled, - donationDialogOpenEvent = { component.analytics.donationDialogVisit() }, - toggleAnalytics = component::toggleAnalytics + toggleAnalytics = component::toggleAnalytics, + openDonationDialog = { + component.analytics.donationDialogVisit() + openDonationDialog() + } ) HomeCategory.History -> HistoryColumn( model.records.sortedByDescending { it.id }, @@ -98,6 +139,7 @@ fun HomeTabBar( } TabRow( + backgroundColor = transparent, selectedTabIndex = selectedIndex, indicator = indicator, modifier = modifier, @@ -195,7 +237,7 @@ fun SearchPanel( fun AboutColumn( modifier: Modifier = Modifier, analyticsEnabled:Boolean, - donationDialogOpenEvent: () -> Unit, + openDonationDialog: () -> Unit, toggleAnalytics: (enabled: Boolean) -> Unit ) { @@ -313,26 +355,9 @@ fun AboutColumn( } } - var isDonationDialogVisible by remember { mutableStateOf(false) } - - DonationDialog( - isDonationDialogVisible, - onDismiss = { - isDonationDialogVisible = false - }, - onSnooze = { - isDonationDialogVisible = false - } - ) - Row( modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) - .clickable( - onClick = { - isDonationDialogVisible = true - donationDialogOpenEvent() - } - ), + .clickable(onClick = openDonationDialog), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Rounded.CardGiftcard, "Support Developer", Modifier.size(32.dp)) @@ -504,7 +529,7 @@ fun HomeCategoryTabIndicator( ) { Spacer( modifier.padding(horizontal = 24.dp) - .height(4.dp) + .height(3.dp) .background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100)) ) } diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt index 3c0242a9..2e82a6de 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.jetbrains.Children import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.crossfadeScale -import com.arkivanov.decompose.extensions.compose.jetbrains.asState +import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.uikit.splash.Splash @@ -125,7 +125,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.d ).then(modifier) ) { - val activeComponent = component.routerState.asState() + val activeComponent = component.routerState.subscribeAsState() val callBacks = component.callBacks AppBar( backgroundColor = appBarColor, diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt index dc98addb..7b7f3db7 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt @@ -1 +1,33 @@ package com.shabinder.common.uikit.dialogs + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.shabinder.common.uikit.DonationDialog + +typealias DonationDialogCallBacks = Triple +private typealias openAction = () -> Unit +private typealias dismissAction = () -> Unit +private typealias snoozeAction = () -> Unit + +@Composable +fun DonationDialogComponent(onDismissExtra: () -> Unit): DonationDialogCallBacks { + var isDonationDialogVisible by remember { mutableStateOf(false) } + DonationDialog( + isDonationDialogVisible, + onSnooze = { isDonationDialogVisible = false }, + onDismiss = { + isDonationDialogVisible = false + } + ) + + val openDonationDialog = { isDonationDialogVisible = true } + val snoozeDonationDialog = { isDonationDialogVisible = false } + val dismissDonationDialog = { + onDismissExtra() + isDonationDialogVisible = false + } + return DonationDialogCallBacks(openDonationDialog,dismissDonationDialog,snoozeDonationDialog) +} \ No newline at end of file diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index d2feb248..037eb808 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -62,7 +62,7 @@ interface SpotiFlyerList { /* * Snooze Donation Dialog * */ - fun snoozeDonationDialog() + fun dismissDonationDialogSetOffset() interface Dependencies { val storeFactory: StoreFactory diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt index 0b4a38d7..6b167dcf 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/integration/SpotiFlyerListImpl.kt @@ -78,7 +78,7 @@ internal class SpotiFlyerListImpl( store.accept(Intent.RefreshTracksStatuses) } - override fun snoozeDonationDialog() { + override fun dismissDonationDialogSetOffset() { preferenceManager.setDonationOffset(offset = 10) } diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 9bfc18ec..d6aa3941 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -59,6 +59,8 @@ interface SpotiFlyerMain { * */ suspend fun loadImage(url: String): Picture + fun dismissDonationDialogOffset() + interface Dependencies { val mainOutput: Consumer val storeFactory: StoreFactory diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index 5872d0c8..fc5ae0b0 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -82,4 +82,8 @@ internal class SpotiFlyerMainImpl( dir.loadImage(url, 150, 150) } } + + override fun dismissDonationDialogOffset() { + preferenceManager.setDonationOffset() + } } diff --git a/common/preference/build.gradle.kts b/common/preference/build.gradle.kts new file mode 100644 index 00000000..329a5b62 --- /dev/null +++ b/common/preference/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +plugins { + id("android-setup") + id("multiplatform-setup") + id("multiplatform-setup-test") + id("kotlin-parcelize") +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":common:dependency-injection")) + implementation(project(":common:data-models")) + implementation(project(":common:database")) + implementation(SqlDelight.coroutineExtensions) + } + } + } +} diff --git a/common/preference/src/androidMain/AndroidManifest.xml b/common/preference/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..ad1a1031 --- /dev/null +++ b/common/preference/src/androidMain/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt new file mode 100644 index 00000000..006e204d --- /dev/null +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/SpotiFlyerPreference.kt @@ -0,0 +1,63 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +package com.shabinder.common.preference + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.shabinder.common.di.Dir +import com.shabinder.common.di.Picture +import com.shabinder.common.di.preference.PreferenceManager +import com.shabinder.common.models.AudioQuality +import com.shabinder.common.models.Consumer +import com.shabinder.common.preference.integration.SpotiFlyerPreferenceImpl + +interface SpotiFlyerPreference { + + val model: Value + + val analytics: Analytics + + fun toggleAnalytics(enabled: Boolean) + + fun setDownloadDirectory(newBasePath: String) + + suspend fun loadImage(url: String): Picture + + interface Dependencies { + val prefOutput: Consumer + val storeFactory: StoreFactory + val dir: Dir + val preferenceManager: PreferenceManager + val preferenceAnalytics: Analytics + } + + interface Analytics + + sealed class Output { + object Finished : Output() + } + + data class State( + val preferredQuality: AudioQuality = AudioQuality.KBPS320, + val isAnalyticsEnabled: Boolean = false + ) +} + +@Suppress("FunctionName") // Factory function +fun SpotiFlyerPreference(componentContext: ComponentContext, dependencies: SpotiFlyerPreference.Dependencies): SpotiFlyerPreference = + SpotiFlyerPreferenceImpl(componentContext, dependencies) diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt new file mode 100644 index 00000000..18b457d8 --- /dev/null +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/integration/SpotiFlyerPreferenceImpl.kt @@ -0,0 +1,71 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +package com.shabinder.common.preference.integration + +import co.touchlab.stately.ensureNeverFrozen +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.value.Value +import com.shabinder.common.caching.Cache +import com.shabinder.common.di.Picture +import com.shabinder.common.di.utils.asValue +import com.shabinder.common.preference.SpotiFlyerPreference +import com.shabinder.common.preference.SpotiFlyerPreference.Dependencies +import com.shabinder.common.preference.SpotiFlyerPreference.State +import com.shabinder.common.preference.store.SpotiFlyerPreferenceStore.Intent +import com.shabinder.common.preference.store.SpotiFlyerPreferenceStoreProvider +import com.shabinder.common.preference.store.getStore + +internal class SpotiFlyerPreferenceImpl( + componentContext: ComponentContext, + dependencies: Dependencies +) : SpotiFlyerPreference, ComponentContext by componentContext, Dependencies by dependencies { + + init { + instanceKeeper.ensureNeverFrozen() + } + + private val store = + instanceKeeper.getStore { + SpotiFlyerPreferenceStoreProvider( + storeFactory = storeFactory, + preferenceManager = preferenceManager + ).provide() + } + + private val cache = Cache.Builder + .newBuilder() + .maximumCacheSize(10) + .build() + + override val model: Value = store.asValue() + + override val analytics = preferenceAnalytics + + override fun toggleAnalytics(enabled: Boolean) { + store.accept(Intent.ToggleAnalytics(enabled)) + } + + override fun setDownloadDirectory(newBasePath: String) { + preferenceManager.setDownloadDirectory(newBasePath) + } + + override suspend fun loadImage(url: String): Picture { + return cache.get(url) { + dir.loadImage(url, 150, 150) + } + } +} diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/InstanceKeeperExt.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/InstanceKeeperExt.kt new file mode 100644 index 00000000..c21de6bf --- /dev/null +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/InstanceKeeperExt.kt @@ -0,0 +1,37 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +package com.shabinder.common.preference.store + +import com.arkivanov.decompose.instancekeeper.InstanceKeeper +import com.arkivanov.decompose.instancekeeper.getOrCreate +import com.arkivanov.mvikotlin.core.store.Store + +fun > InstanceKeeper.getStore(key: Any, factory: () -> T): T = + getOrCreate(key) { StoreHolder(factory()) } + .store + +inline fun > InstanceKeeper.getStore(noinline factory: () -> T): T = + getStore(T::class, factory) + +private class StoreHolder>( + val store: T +) : InstanceKeeper.Instance { + override fun onDestroy() { + store.dispose() + } +} diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt new file mode 100644 index 00000000..054fff9d --- /dev/null +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStore.kt @@ -0,0 +1,29 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +package com.shabinder.common.preference.store + +import com.arkivanov.mvikotlin.core.store.Store +import com.shabinder.common.preference.SpotiFlyerPreference + +internal interface SpotiFlyerPreferenceStore : Store { + sealed class Intent { + data class OpenPlatform(val platformID: String, val platformLink: String) : Intent() + data class ToggleAnalytics(val enabled: Boolean) : Intent() + object GiveDonation : Intent() + object ShareApp : Intent() + } +} diff --git a/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStoreProvider.kt b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStoreProvider.kt new file mode 100644 index 00000000..ba273448 --- /dev/null +++ b/common/preference/src/commonMain/kotlin/com/shabinder/common/preference/store/SpotiFlyerPreferenceStoreProvider.kt @@ -0,0 +1,73 @@ +/* + * * Copyright (c) 2021 Shabinder Singh + * * This program is free software: you can redistribute it and/or modify + * * it under the terms of the GNU General Public License as published by + * * the Free Software Foundation, either version 3 of the License, or + * * (at your option) any later version. + * * + * * This program is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU General Public License for more details. + * * + * * You should have received a copy of the GNU General Public License + * * along with this program. If not, see . + */ + +package com.shabinder.common.preference.store + +import com.arkivanov.mvikotlin.core.store.Reducer +import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper +import com.arkivanov.mvikotlin.core.store.Store +import com.arkivanov.mvikotlin.core.store.StoreFactory +import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor +import com.shabinder.common.di.preference.PreferenceManager +import com.shabinder.common.models.methods +import com.shabinder.common.preference.SpotiFlyerPreference.State +import com.shabinder.common.preference.store.SpotiFlyerPreferenceStore.Intent + +internal class SpotiFlyerPreferenceStoreProvider( + private val storeFactory: StoreFactory, + private val preferenceManager: PreferenceManager +) { + + fun provide(): SpotiFlyerPreferenceStore = + object : + SpotiFlyerPreferenceStore, + Store by storeFactory.create( + name = "SpotiFlyerPreferenceStore", + initialState = State(), + bootstrapper = SimpleBootstrapper(Unit), + executorFactory = ::ExecutorImpl, + reducer = ReducerImpl + ) {} + + private sealed class Result { + data class ToggleAnalytics(val isEnabled: Boolean) : Result() + } + + private inner class ExecutorImpl : SuspendExecutor() { + override suspend fun executeAction(action: Unit, getState: () -> State) { + dispatch(Result.ToggleAnalytics(preferenceManager.isAnalyticsEnabled)) + } + + override suspend fun executeIntent(intent: Intent, getState: () -> State) { + when (intent) { + is Intent.OpenPlatform -> methods.value.openPlatform(intent.platformID, intent.platformLink) + is Intent.GiveDonation -> methods.value.giveDonation() + is Intent.ShareApp -> methods.value.shareApp() + is Intent.ToggleAnalytics -> { + dispatch(Result.ToggleAnalytics(intent.enabled)) + preferenceManager.toggleAnalytics(intent.enabled) + } + } + } + } + + private object ReducerImpl : Reducer { + override fun State.reduce(result: Result): State = + when (result) { + is Result.ToggleAnalytics -> copy(isAnalyticsEnabled = result.isEnabled) + } + } +} From e50cf82f6a5416333c2d9ba4dc1983e1055ec61f Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 25 Jun 2021 18:34:28 +0530 Subject: [PATCH 13/15] Internationalization WIP --- buildSrc/build.gradle.kts | 3 +- buildSrc/buildSrc/src/main/kotlin/Versions.kt | 4 ++ .../common/uikit/SpotiFlyerMainUi.kt | 69 ++++++++++--------- .../common/uikit/SpotiFlyerRootUi.kt | 9 +-- common/data-models/build.gradle.kts | 10 +++ .../kotlin/com/shabinder/common/Ext.kt | 2 +- translations/Strings_en.properties | 35 ++++++++++ 7 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 translations/Strings_en.properties diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 48ae8147..6a9fa555 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -31,11 +31,12 @@ repositories { dependencies { implementation("com.android.tools.build:gradle:4.1.1") - implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}") implementation(JetBrains.Compose.gradlePlugin) implementation(JetBrains.Kotlin.gradlePlugin) implementation(JetBrains.Kotlin.serialization) implementation(SqlDelight.gradlePlugin) + implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}") + implementation("de.comahe.i18n4k:i18n4k-gradle-plugin:0.1.1") } kotlin { diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt index fbb3f1b9..6e64166a 100644 --- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt @@ -145,6 +145,10 @@ object Ktor { val clientJs = "io.ktor:ktor-client-js:${Versions.ktor}" } +object Internationalization { + const val dep = "de.comahe.i18n4k:i18n4k-core:0.1.1" +} + object Extras { const val youtubeDownloader = "io.github.shabinder:youtube-api-dl:1.2" const val fuzzyWuzzy = "io.github.shabinder:fuzzywuzzy:1.1" diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt index 8fe52a33..76ee29f5 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt @@ -83,6 +83,7 @@ import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain.HomeCategory import com.shabinder.common.models.DownloadRecord import com.shabinder.common.models.methods +import com.shabinder.common.translations.Strings import com.shabinder.common.uikit.dialogs.DonationDialogComponent @Composable @@ -151,16 +152,16 @@ fun HomeTabBar( text = { Text( text = when (category) { - HomeCategory.About -> "About" - HomeCategory.History -> "History" + HomeCategory.About -> Strings.about() + HomeCategory.History -> Strings.history() }, style = MaterialTheme.typography.body2 ) }, icon = { when (category) { - HomeCategory.About -> Icon(Icons.Outlined.Info, "Info Tab") - HomeCategory.History -> Icon(Icons.Outlined.History, "History Tab") + HomeCategory.About -> Icon(Icons.Outlined.Info, Strings.infoTab()) + HomeCategory.History -> Icon(Icons.Outlined.History, Strings.historyTab()) } } ) @@ -183,9 +184,9 @@ fun SearchPanel( value = link, onValueChange = updateLink, leadingIcon = { - Icon(Icons.Rounded.Edit, "Link Text Box", tint = Color.LightGray) + Icon(Icons.Rounded.Edit, Strings.linkTextBox(), tint = Color.LightGray) }, - label = { Text(text = "Paste Link Here...", color = Color.LightGray) }, + label = { Text(text = Strings.pasteLinkHere(), color = Color.LightGray) }, singleLine = true, textStyle = TextStyle.Default.merge(TextStyle(fontSize = 18.sp, color = Color.White)), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), @@ -212,7 +213,7 @@ fun SearchPanel( OutlinedButton( modifier = Modifier.padding(12.dp).wrapContentWidth(), onClick = { - if (link.isBlank()) methods.value.showPopUpMessage("Enter A Link!") + if (link.isBlank()) methods.value.showPopUpMessage(Strings.enterALink()) else { // TODO if(!isOnline(ctx)) showPopUpMessage("Check Your Internet Connection") else onSearch(link) @@ -228,7 +229,7 @@ fun SearchPanel( ) ) ) { - Text(text = "Search", style = SpotiFlyerTypography.h6, modifier = Modifier.padding(4.dp)) + Text(text = Strings.search(), style = SpotiFlyerTypography.h6, modifier = Modifier.padding(4.dp)) } } } @@ -251,7 +252,7 @@ fun AboutColumn( ) { Column(modifier.padding(12.dp)) { Text( - text = "Supported Platforms", + text = Strings.supportedPlatforms(), style = SpotiFlyerTypography.body1, color = colorAccent ) @@ -259,7 +260,7 @@ fun AboutColumn( Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) { Icon( SpotifyLogo(), - "Open Spotify", + "${Strings.open()} Spotify", tint = Color.Unspecified, modifier = Modifier.clip(SpotiFlyerShapes.small).clickable( onClick = { methods.value.openPlatform("com.spotify.music", "http://open.spotify.com") } @@ -268,7 +269,7 @@ fun AboutColumn( Spacer(modifier = modifier.padding(start = 16.dp)) Icon( GaanaLogo(), - "Open Gaana", + "${Strings.open()} Gaana", tint = Color.Unspecified, modifier = Modifier.clip(SpotiFlyerShapes.small).clickable( onClick = { methods.value.openPlatform("com.gaana", "https://www.gaana.com") } @@ -277,7 +278,7 @@ fun AboutColumn( Spacer(modifier = modifier.padding(start = 16.dp)) Icon( SaavnLogo(), - "Open Jio Saavn", + "${Strings.open()} Jio Saavn", tint = Color.Unspecified, modifier = Modifier.clickable( onClick = { methods.value.openPlatform("com.jio.media.jiobeats", "https://www.jiosaavn.com/") } @@ -286,7 +287,7 @@ fun AboutColumn( Spacer(modifier = modifier.padding(start = 16.dp)) Icon( YoutubeLogo(), - "Open Youtube", + "${Strings.open()} Youtube", tint = Color.Unspecified, modifier = Modifier.clip(SpotiFlyerShapes.small).clickable( onClick = { methods.value.openPlatform("com.google.android.youtube", "https://m.youtube.com") } @@ -295,7 +296,7 @@ fun AboutColumn( Spacer(modifier = modifier.padding(start = 12.dp)) Icon( YoutubeMusicLogo(), - "Open Youtube Music", + "${Strings.open()} Youtube Music", tint = Color.Unspecified, modifier = Modifier.clip(SpotiFlyerShapes.small).clickable( onClick = { methods.value.openPlatform("com.google.android.apps.youtube.music", "https://music.youtube.com/") } @@ -311,7 +312,7 @@ fun AboutColumn( ) { Column(modifier.padding(12.dp)) { Text( - text = "Support Development", + text = Strings.supportDevelopment(), style = SpotiFlyerTypography.body1, color = colorAccent ) @@ -323,7 +324,7 @@ fun AboutColumn( ) .padding(vertical = 6.dp) ) { - Icon(GithubLogo(), "Open Project Repo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC)) + Icon(GithubLogo(), Strings.openProjectRepo(), Modifier.size(32.dp), tint = Color(0xFFCCCCCC)) Spacer(modifier = Modifier.padding(start = 16.dp)) Column { Text( @@ -331,7 +332,7 @@ fun AboutColumn( style = SpotiFlyerTypography.h6 ) Text( - text = "Star / Fork the project on Github.", + text = Strings.starOrForkProject(), style = SpotiFlyerTypography.subtitle2 ) } @@ -341,15 +342,15 @@ fun AboutColumn( .clickable(onClick = { methods.value.openPlatform("", "http://github.com/Shabinder/SpotiFlyer") }), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.Flag, "Help Translate", Modifier.size(32.dp)) + Icon(Icons.Rounded.Flag, Strings.help() + Strings.translate(), Modifier.size(32.dp)) Spacer(modifier = Modifier.padding(start = 16.dp)) Column { Text( - text = "Translate", + text = Strings.translate(), style = SpotiFlyerTypography.h6 ) Text( - text = "Help us translate this app in your local language.", + text = Strings.helpTranslateDescription(), style = SpotiFlyerTypography.subtitle2 ) } @@ -360,15 +361,15 @@ fun AboutColumn( .clickable(onClick = openDonationDialog), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.CardGiftcard, "Support Developer", Modifier.size(32.dp)) + Icon(Icons.Rounded.CardGiftcard, Strings.supportDeveloper(), Modifier.size(32.dp)) Spacer(modifier = Modifier.padding(start = 16.dp)) Column { Text( - text = "Donate", + text = Strings.donate(), style = SpotiFlyerTypography.h6 ) Text( - text = "If you think I deserve to get paid for my work, you can support me here.", + text = Strings.donateDescription(), // text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.", style = SpotiFlyerTypography.subtitle2 ) @@ -383,15 +384,15 @@ fun AboutColumn( ), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.Share, "Share SpotiFlyer App", Modifier.size(32.dp)) + Icon(Icons.Rounded.Share, Strings.share() + Strings.title() + "App", Modifier.size(32.dp)) Spacer(modifier = Modifier.padding(start = 16.dp)) Column { Text( - text = "Share", + text = Strings.share(), style = SpotiFlyerTypography.h6 ) Text( - text = "Share this app with your friends and family.", + text = Strings.shareDescription(), style = SpotiFlyerTypography.subtitle2 ) } @@ -405,17 +406,17 @@ fun AboutColumn( ), verticalAlignment = Alignment.CenterVertically ) { - Icon(Icons.Rounded.Insights, "Analytics Status", Modifier.size(32.dp)) + Icon(Icons.Rounded.Insights, Strings.analytics() + Strings.status(), Modifier.size(32.dp)) Spacer(modifier = Modifier.padding(start = 16.dp)) Column( Modifier.weight(1f) ) { Text( - text = "Analytics", + text = Strings.analytics(), style = SpotiFlyerTypography.h6 ) Text( - text = "Your Data is Anonymized and never shared with 3rd party service", + text = Strings.analyticsDescription(), style = SpotiFlyerTypography.subtitle2 ) } @@ -446,10 +447,10 @@ fun HistoryColumn( if (it.isEmpty()) { Column(Modifier.padding(8.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Icon( - Icons.Outlined.Info, "No History Available Yet", modifier = Modifier.size(80.dp), + Icons.Outlined.Info, Strings.noHistoryAvailable(), modifier = Modifier.size(80.dp), colorOffWhite ) - Text("No History Available", style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light), textAlign = TextAlign.Center) + Text(Strings.noHistoryAvailable(), style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light), textAlign = TextAlign.Center) } } else { Box { @@ -495,7 +496,7 @@ fun DownloadRecordItem( ImageLoad( item.coverUrl, { loadImage(item.coverUrl) }, - "Album Art", + Strings.albumArt(), modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium) ) Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f), verticalArrangement = Arrangement.SpaceEvenly) { @@ -506,12 +507,12 @@ fun DownloadRecordItem( modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() ) { Text(item.type, fontSize = 13.sp, color = colorOffWhite) - Text("Tracks: ${item.totalFiles}", fontSize = 13.sp, color = colorOffWhite) + Text("${Strings.tracks()}: ${item.totalFiles}", fontSize = 13.sp, color = colorOffWhite) } } Image( ShareImage(), - "Research", + Strings.reSearch(), modifier = Modifier.clickable( onClick = { // if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt index 2e82a6de..ad0112b6 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt @@ -59,6 +59,7 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.cros import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot.Child +import com.shabinder.common.translations.Strings import com.shabinder.common.uikit.splash.Splash import com.shabinder.common.uikit.splash.SplashState import com.shabinder.common.uikit.utils.verticalGradientScrim @@ -163,7 +164,7 @@ fun AppBar( AnimatedVisibility(isBackButtonVisible) { Icon( Icons.Rounded.ArrowBackIosNew, - contentDescription = "Back Button", + contentDescription = Strings.backButton(), modifier = Modifier.clickable { onBackPressed() }, tint = Color.LightGray ) @@ -171,12 +172,12 @@ fun AppBar( } Image( SpotiFlyerLogo(), - "SpotiFlyer Logo", + Strings.spotiflyerLogo(), Modifier.size(32.dp), ) Spacer(Modifier.padding(horizontal = 4.dp)) Text( - text = "SpotiFlyer", + text = Strings.title(), style = appNameStyle ) } @@ -185,7 +186,7 @@ fun AppBar( IconButton( onClick = { setDownloadDirectory() } ) { - Icon(Icons.Filled.Settings, "Preferences", tint = Color.Gray) + Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray) } }, modifier = modifier, diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts index 5f8dd925..2499b0ad 100644 --- a/common/data-models/build.gradle.kts +++ b/common/data-models/build.gradle.kts @@ -1,3 +1,5 @@ +import de.comahe.i18n4k.gradle.plugin.i18n4k + /* * * Copyright (c) 2021 Shabinder Singh * * This program is free software: you can redistribute it and/or modify @@ -20,11 +22,18 @@ plugins { id("multiplatform-setup-test") id("kotlin-parcelize") kotlin("plugin.serialization") + id("de.comahe.i18n4k") } val statelyVersion = "1.1.7" val statelyIsoVersion = "1.1.7-a1" +i18n4k { + inputDirectory = "../../translations" + packageName = "com.shabinder.common.translations" + // sourceCodeLocales = listOf("en", "de") +} + kotlin { sourceSets { /* @@ -45,6 +54,7 @@ kotlin { implementation("co.touchlab:stately-isolate:$statelyIsoVersion") implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion") implementation(Extras.youtubeDownloader) + api(Internationalization.dep) } } androidMain { diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt index 66717056..98e0e23f 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt @@ -1,3 +1,3 @@ package com.shabinder.common -fun T?.requireNotNull() : T = requireNotNull(this) \ No newline at end of file +fun T?.requireNotNull() : T = requireNotNull(this) diff --git a/translations/Strings_en.properties b/translations/Strings_en.properties new file mode 100644 index 00000000..ddbe59f1 --- /dev/null +++ b/translations/Strings_en.properties @@ -0,0 +1,35 @@ +title = SpotiFlyer +about = About +history = History +donate = Donate +preferences = Preferences +search = Search +supportedPlatforms = Supported Platforms +supportDevelopment = Support Development +openProjectRepo = Open Project Repo +starOrForkProject = Star / Fork the project on Github. +help = Help +translate = Translate +helpTranslateDescription = Help us translate this app in your local language. +supportDeveloper = Support Developer +donateDescription = If you think I deserve to get paid for my work, you can support me here. +share = Share +shareDescription = Share this app with your friends and family. +status = Status +analytics = Analytics +analyticsDescription = Your Data is Anonymized and never shared with 3rd party service. +noHistoryAvailable = No History Available + +albumArt = Album Art +tracks = Tracks +reSearch = Re-Search +spotiflyerLogo = SpotiFlyer Logo +backButton = Back Button +infoTab = Info Tab +historyTab = History Tab +linkTextBox = Link Text Box +pasteLinkHere = Paste Link Here... +enterALink = Enter A Link! +madeWith = Made with +inIndia = in India +open = Open From 6e517e004907b9fd1f1334ad35383322a3575183 Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 25 Jun 2021 19:16:38 +0530 Subject: [PATCH 14/15] Internationalization WIP --- .../main/java/com/shabinder/spotiflyer/App.kt | 9 +++-- .../spotiflyer/service/ForegroundService.kt | 7 ++-- .../shabinder/spotiflyer/service/Message.kt | 11 ++--- android/src/main/res/values/strings.xml | 37 ----------------- .../shabinder/common/uikit/AndroidImages.kt | 7 ++-- .../shabinder/common/uikit/DonationDialog.kt | 13 +++--- .../common/uikit/SpotiFlyerListUi.kt | 15 +++---- .../shabinder/common/uikit/splash/Splash.kt | 11 ++--- .../shabinder/common/uikit/DonationDialog.kt | 8 ++-- .../common/models/SpotiFlyerException.kt | 18 +++++---- translations/Strings_en.properties | 40 +++++++++++++++++++ 11 files changed, 94 insertions(+), 82 deletions(-) delete mode 100644 android/src/main/res/values/strings.xml diff --git a/android/src/main/java/com/shabinder/spotiflyer/App.kt b/android/src/main/java/com/shabinder/spotiflyer/App.kt index 16d02984..b4de2a9d 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/App.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/App.kt @@ -19,6 +19,7 @@ package com.shabinder.spotiflyer import android.app.Application import android.content.Context import com.shabinder.common.di.initKoin +import com.shabinder.common.translations.Strings import com.shabinder.spotiflyer.di.appModule import org.acra.config.httpSender import org.acra.config.notification @@ -77,10 +78,10 @@ class App: Application(), KoinComponent { * Obeying `F-Droid Inclusion Privacy Rules` * */ notification { - title = getString(R.string.acra_notification_title) - text = getString(R.string.acra_notification_text) - channelName = getString(R.string.acra_notification_channel) - channelDescription = getString(R.string.acra_notification_channel_desc) + title = Strings.acraNotificationTitle() + text = Strings.acraNotificationText() + channelName = "SpotiFlyer_Crashlytics" + channelDescription = "Notification Channel to send Spotiflyer Crashes." sendOnClick = true } // Send Crash Report to self hosted Acrarium (FOSS) diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt index 3d57d70c..1d2f7540 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt @@ -43,6 +43,7 @@ import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.failure +import com.shabinder.common.translations.Strings import com.shabinder.spotiflyer.utils.autoclear.AutoClear import com.shabinder.spotiflyer.utils.autoclear.autoClear import kotlinx.coroutines.Dispatchers @@ -237,7 +238,7 @@ class ForegroundService : LifecycleService() { lifecycleScope.launch { logger.d(TAG) { "Killing Self" } messageList = messageList.getEmpty().apply { - set(index = 0, Message("Cleaning And Exiting",DownloadStatus.NotDownloaded)) + set(index = 0, Message(Strings.cleaningAndExiting(),DownloadStatus.NotDownloaded)) } downloadService.value.close() downloadService.reset() @@ -257,7 +258,7 @@ class ForegroundService : LifecycleService() { private fun createNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run { setSmallIcon(R.drawable.ic_download_arrow) - setContentTitle("Total: $total Completed:$converted Failed:$failed") + setContentTitle("${Strings.total()}: $total ${Strings.completed()}:$converted ${Strings.failed()}:$failed") setSilent(true) setProgress(total,failed+converted,false) setStyle( @@ -269,7 +270,7 @@ class ForegroundService : LifecycleService() { addLine(messageList[messageList.size - 5].asString()) } ) - addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent) + addAction(R.drawable.ic_round_cancel_24, Strings.exit(), cancelIntent) build() } diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt index 0e03615e..77a19894 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt @@ -1,6 +1,7 @@ package com.shabinder.spotiflyer.service import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.translations.Strings typealias Message = Pair @@ -11,9 +12,9 @@ val Message.downloadStatus: DownloadStatus get() = second val Message.progress: String get() = when (downloadStatus) { is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%" is DownloadStatus.Converting -> "-> 100%" - is DownloadStatus.Downloaded -> "-> Done" - is DownloadStatus.Failed -> "-> Failed" - is DownloadStatus.Queued -> "-> Queued" + is DownloadStatus.Downloaded -> "-> ${Strings.downloadDone}" + is DownloadStatus.Failed -> "-> ${Strings.failed()}" + is DownloadStatus.Queued -> "-> ${Strings.queued()}" is DownloadStatus.NotDownloaded -> "" } @@ -23,8 +24,8 @@ val emptyMessage = Message("",DownloadStatus.NotDownloaded) // all Progress data is emitted all together from fun fun Message.asString(): String { val statusString = when(downloadStatus){ - is DownloadStatus.Downloading -> "Downloading" - is DownloadStatus.Converting -> "Processing" + is DownloadStatus.Downloading -> Strings.downloading() + is DownloadStatus.Converting -> Strings.processing() else -> "" } return "$statusString $title ${""/*progress*/}".trim() diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml deleted file mode 100644 index c0eb149b..00000000 --- a/android/src/main/res/values/strings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - SpotiFlyer - About - History - Supported Platforms - Support Development - Star / Fork the project on Github. - GitHub - Translate - Help us translate this app in your local language. - Donate - If you think I deserve to get paid for my work, you can leave me some money here. - Share - Share this app with your friends and family. - Made with - in India - OOPS, SpotiFlyer Crashed - Please Send Crash Report to App Developers, So this unfortunate event may not happen again. - SpotiFlyer_Crashlytics - Notification Channel to send Spotiflyer Crashes. - \ No newline at end of file diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt index 47cc0d40..10ea5bf5 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import com.shabinder.common.database.R +import com.shabinder.common.translations.Strings import kotlinx.coroutines.flow.MutableStateFlow actual fun montserratFont() = FontFamily( @@ -43,7 +44,7 @@ actual fun pristineFont() = FontFamily( actual fun DownloadImageTick() { Image( painterResource(R.drawable.ic_tick), - "Download Done" + Strings.downloadDone() ) } @@ -51,7 +52,7 @@ actual fun DownloadImageTick() { actual fun DownloadImageError() { Image( painterResource(R.drawable.ic_error), - "Error! Cant Download this track" + Strings.downloadError() ) } @@ -59,7 +60,7 @@ actual fun DownloadImageError() { actual fun DownloadImageArrow(modifier: Modifier) { Image( painterResource(R.drawable.ic_arrow), - "Start Download", + Strings.downloadStart(), modifier ) } diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt index 5ad20017..ceae4b23 100644 --- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt +++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.shabinder.common.models.methods +import com.shabinder.common.translations.Strings @OptIn(ExperimentalAnimationApi::class) @Composable @@ -44,7 +45,7 @@ actual fun DonationDialog( ) { Column(Modifier.padding(16.dp)) { Text( - "We Need Your Support!", + Strings.supportUs(), style = SpotiFlyerTypography.h5, textAlign = TextAlign.Center, color = colorAccent, @@ -69,7 +70,7 @@ actual fun DonationDialog( style = SpotiFlyerTypography.h6 ) Text( - text = "Worldwide Donations", + text = Strings.worldWideDonations(), style = SpotiFlyerTypography.subtitle2 ) } @@ -92,7 +93,7 @@ actual fun DonationDialog( style = SpotiFlyerTypography.h6 ) Text( - text = "International Donations (Outside India).", + text = Strings.worldWideDonations(), style = SpotiFlyerTypography.subtitle2 ) } @@ -115,7 +116,7 @@ actual fun DonationDialog( style = SpotiFlyerTypography.h6 ) Text( - text = "Indian Donations (UPI / PayTM / PhonePe / Cards).", + text = "${Strings.indianDonations()} (UPI / PayTM / PhonePe / Cards).", style = SpotiFlyerTypography.subtitle2 ) } @@ -127,10 +128,10 @@ actual fun DonationDialog( modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth() ) { OutlinedButton(onClick = onDismiss) { - Text("Dismiss.") + Text(Strings.dismiss()) } TextButton(onClick = onSnooze, colors = ButtonDefaults.buttonColors()) { - Text("Remind Later!") + Text(Strings.remindLater()) } } } diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt index 8df74b56..006d2054 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt @@ -54,6 +54,7 @@ import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods +import com.shabinder.common.translations.Strings import com.shabinder.common.uikit.dialogs.DonationDialogComponent @OptIn(ExperimentalMaterialApi::class) @@ -67,7 +68,7 @@ fun SpotiFlyerListContent( LaunchedEffect(model.errorOccurred) { /*Handle if Any Exception Occurred*/ model.errorOccurred?.let { - methods.value.showPopUpMessage(it.message ?: "An Error Occurred, Check your Link / Connection") + methods.value.showPopUpMessage(it.message ?: Strings.errorOccurred()) component.onBackPressed() } } @@ -79,7 +80,7 @@ fun SpotiFlyerListContent( Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator() Spacer(modifier.padding(8.dp)) - Text("Loading..", style = appNameStyle, color = colorPrimary) + Text("${Strings.loading()}...", style = appNameStyle, color = colorPrimary) } } else { @@ -142,7 +143,7 @@ fun TrackCard( ImageLoad( track.albumArtURL, { loadImage() }, - "Album Art", + Strings.albumArt(), modifier = Modifier .width(70.dp) .height(70.dp) @@ -156,7 +157,7 @@ fun TrackCard( modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() ) { Text("${track.artists.firstOrNull()}...", fontSize = 12.sp, maxLines = 1) - Text("${track.durationSec / 60} min, ${track.durationSec % 60} sec", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text("${track.durationSec / 60} ${Strings.minute()}, ${track.durationSec % 60} ${Strings.second()}", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } } when (track.downloaded) { @@ -202,7 +203,7 @@ fun CoverImage( ImageLoad( coverURL, { loadImage(coverURL, true) }, - "Cover Image", + Strings.coverImage(), modifier = Modifier .padding(12.dp) .width(190.dp) @@ -225,9 +226,9 @@ fun CoverImage( @Composable fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) { ExtendedFloatingActionButton( - text = { Text("Download All") }, + text = { Text(Strings.downloadAll()) }, onClick = onClick, - icon = { Icon(DownloadAllImage(), "Download All Button", tint = Color(0xFF000000)) }, + icon = { Icon(DownloadAllImage(), Strings.downloadAll() + Strings.button(), tint = Color(0xFF000000)) }, backgroundColor = colorAccent, modifier = modifier ) diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt index 502bf4a3..6632031e 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.shabinder.common.translations.Strings import com.shabinder.common.uikit.HeartIcon import com.shabinder.common.uikit.SpotiFlyerLogo import com.shabinder.common.uikit.SpotiFlyerTypography @@ -55,7 +56,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) { delay(SplashWaitTime) currentOnTimeout() } - Image(SpotiFlyerLogo(), "SpotiFlyer Logo") + Image(SpotiFlyerLogo(), Strings.spotiflyerLogo()) MadeInIndia(Modifier.align(Alignment.BottomCenter)) } } @@ -73,21 +74,21 @@ fun MadeInIndia( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "Made with ", + text = "${Strings.madeWith()} ", color = colorPrimary, fontSize = 22.sp ) Spacer(modifier = Modifier.padding(start = 4.dp)) - Icon(HeartIcon(), "Love", tint = Color.Unspecified) + Icon(HeartIcon(), Strings.love(), tint = Color.Unspecified) Spacer(modifier = Modifier.padding(start = 4.dp)) Text( - text = " in India", + text = " ${Strings.inIndia()}", color = colorPrimary, fontSize = 22.sp ) } Text( - "by: Shabinder Singh", + Strings.byDeveloperName(), style = SpotiFlyerTypography.h6, color = colorAccent, fontSize = 14.sp diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt index 19fe6600..e0e692e2 100644 --- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt +++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt @@ -20,9 +20,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.v1.Dialog import com.shabinder.common.models.methods +import com.shabinder.common.translations.Strings @OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable @@ -42,7 +42,7 @@ actual fun DonationDialog( ) { Column(Modifier.padding(16.dp)) { Text( - "Support Us", + Strings.supportUs(), style = SpotiFlyerTypography.h5, textAlign = TextAlign.Center, color = colorAccent, @@ -67,7 +67,7 @@ actual fun DonationDialog( style = SpotiFlyerTypography.h6 ) Text( - text = "International Donations (Outside India).", + text = Strings.worldWideDonations(), style = SpotiFlyerTypography.subtitle2 ) } @@ -90,7 +90,7 @@ actual fun DonationDialog( style = SpotiFlyerTypography.h6 ) Text( - text = "Indian Donations (UPI / PayTM / PhonePe / Cards).", + text = "${Strings.indianDonations()} (UPI / PayTM / PhonePe / Cards).", style = SpotiFlyerTypography.subtitle2 ) } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt index 0374c95b..2af34bd1 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt @@ -1,41 +1,43 @@ package com.shabinder.common.models +import com.shabinder.common.translations.Strings + sealed class SpotiFlyerException(override val message: String): Exception(message) { - data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message) - data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message) + data class FeatureNotImplementedYet(override val message: String = Strings.featureUnImplemented()): SpotiFlyerException(message) + data class NoInternetException(override val message: String = Strings.checkInternetConnection()): SpotiFlyerException(message) data class MP3ConversionFailed( val extraInfo:String? = null, - override val message: String = "MP3 Converter unreachable, probably BUSY ! \nCAUSE:$extraInfo" + override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo" ): SpotiFlyerException(message) data class UnknownReason( val exception: Throwable? = null, - override val message: String = "Unknown Error" + override val message: String = Strings.unknownError() ): SpotiFlyerException(message) data class NoMatchFound( val trackName: String? = null, - override val message: String = "$trackName : NO Match Found!" + override val message: String = "$trackName : ${Strings.noMatchFound()}" ): SpotiFlyerException(message) data class YoutubeLinkNotFound( val videoID: String? = null, - override val message: String = "No Downloadable link found for videoID: $videoID" + override val message: String = "${Strings.noLinkFound()}: $videoID" ): SpotiFlyerException(message) data class DownloadLinkFetchFailed( val trackName: String, val jioSaavnError: Throwable, val ytMusicError: Throwable, - override val message: String = "No Downloadable link found for track: $trackName," + + override val message: String = "${Strings.noLinkFound()}: $trackName," + " \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n " + " \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " ): SpotiFlyerException(message) data class LinkInvalid( val link: String? = null, - override val message: String = "Entered Link is NOT Valid!\n ${link ?: ""}" + override val message: String = "${Strings.linkNotValid()}\n ${link ?: ""}" ): SpotiFlyerException(message) } \ No newline at end of file diff --git a/translations/Strings_en.properties b/translations/Strings_en.properties index ddbe59f1..fc85e955 100644 --- a/translations/Strings_en.properties +++ b/translations/Strings_en.properties @@ -19,10 +19,48 @@ status = Status analytics = Analytics analyticsDescription = Your Data is Anonymized and never shared with 3rd party service. noHistoryAvailable = No History Available +cleaningAndExiting = Cleaning And Exiting +total = Total +completed = Completed +failed = Failed +exit = Exit +downloading = Downloading +processing = Processing +queued = Queued + +acraNotificationTitle = OOPS, SpotiFlyer Crashed +acraNotificationText = Please Send Crash Report to App Developers, So this unfortunate event may not happen again. albumArt = Album Art tracks = Tracks +coverImage = Cover Image reSearch = Re-Search +loading = Loading +downloadAll = Download All +button = Button +errorOccurred = An Error Occurred, Check your Link / Connection +downloadDone = Download Done +downloadError = Error! Cant Download this track +downloadStart = Start Download +supportUs = We Need Your Support! +donation = Donation +worldWideDonations = World Wide Donations +indianDonations = Indian Donations Only +dismiss = Dismiss +remindLater = Remind Later + +# Exceptions +mp3ConverterBusy = MP3 Converter unreachable, probably BUSY ! +unknownError = Unknown Error +noMatchFound = NO Match Found! +noLinkFound = No Downloadable link found +linkNotValid = Entered Link is NOT Valid! +checkInternetConnection = Check Your Internet Connection +featureUnImplemented = Feature not yet implemented. + +minute = min +second = sec + spotiflyerLogo = SpotiFlyer Logo backButton = Back Button infoTab = Info Tab @@ -31,5 +69,7 @@ linkTextBox = Link Text Box pasteLinkHere = Paste Link Here... enterALink = Enter A Link! madeWith = Made with +love = Love inIndia = in India open = Open +byDeveloperName = by: Shabinder Singh \ No newline at end of file From 5102a8ea485c41deb9d51f6df3a7184be21a4cf7 Mon Sep 17 00:00:00 2001 From: shabinder Date: Sat, 26 Jun 2021 00:33:43 +0530 Subject: [PATCH 15/15] Console App (Folder) & Mosaic-SubModule --- .gitmodules | 3 + console-app/build.gradle.kts | 59 +++++++++++++++++++ console-app/src/main/java/main.kt | 20 +++++++ console-app/src/main/java/utils/Exceptions.kt | 17 ++++++ console-app/src/main/java/utils/Ext.kt | 9 +++ console-app/src/main/java/utils/TestClass.kt | 6 ++ settings.gradle.kts | 2 +- 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 console-app/build.gradle.kts create mode 100644 console-app/src/main/java/main.kt create mode 100644 console-app/src/main/java/utils/Exceptions.kt create mode 100644 console-app/src/main/java/utils/Ext.kt create mode 100644 console-app/src/main/java/utils/TestClass.kt diff --git a/.gitmodules b/.gitmodules index d89eb58f..6b523bd4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "spotiflyer-ios"] path = spotiflyer-ios url = https://github.com/Shabinder/spotiflyer-ios +[submodule "mosaic"] + path = mosaic + url = https://github.com/JakeWharton/mosaic diff --git a/console-app/build.gradle.kts b/console-app/build.gradle.kts new file mode 100644 index 00000000..ec9a4d43 --- /dev/null +++ b/console-app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + kotlin("jvm")// version "1.4.32" + kotlin("plugin.serialization") + id("ktlint-setup") + id("com.jakewharton.mosaic") + application +} + +group = "com.shabinder" +version = Versions.versionCode + +repositories { + mavenCentral() +} + +application { + mainClass.set("MainKt") + applicationName = "spotiflyer-console-app" +} + +dependencies { + implementation(Koin.core) + implementation(project(":common:database")) + implementation(project(":common:data-models")) + implementation(project(":common:dependency-injection")) + implementation(project(":common:root")) + implementation(project(":common:main")) + implementation(project(":common:list")) + implementation(project(":common:list")) + + + // Decompose + implementation(Decompose.decompose) + implementation(Decompose.extensionsCompose) + + // MVI + implementation(MVIKotlin.mvikotlin) + implementation(MVIKotlin.mvikotlinMain) + + // Koin + implementation(Koin.core) + + // Matomo + implementation("org.piwik.java.tracking:matomo-java-tracker:1.6") + + implementation(Ktor.slf4j) + implementation(Ktor.clientCore) + implementation(Ktor.clientJson) + implementation(Ktor.clientApache) + implementation(Ktor.clientLogging) + implementation(Ktor.clientSerialization) + implementation(Serialization.json) + // testDeps + testImplementation(kotlin("test-junit")) +} + +tasks.test { + useJUnit() +} diff --git a/console-app/src/main/java/main.kt b/console-app/src/main/java/main.kt new file mode 100644 index 00000000..62c5bdea --- /dev/null +++ b/console-app/src/main/java/main.kt @@ -0,0 +1,20 @@ +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.jakewharton.mosaic.Text +import com.jakewharton.mosaic.runMosaic +import kotlinx.coroutines.delay + +fun main(/*args: Array*/) = runMosaic { + // TODO https://github.com/JakeWharton/mosaic/issues/3 + var count by mutableStateOf(0) + + setContent { + Text("The count is: $count") + } + + for (i in 1..20) { + delay(250) + count = i + } +} diff --git a/console-app/src/main/java/utils/Exceptions.kt b/console-app/src/main/java/utils/Exceptions.kt new file mode 100644 index 00000000..ed4d8751 --- /dev/null +++ b/console-app/src/main/java/utils/Exceptions.kt @@ -0,0 +1,17 @@ +@file:Suppress("ClassName") + +package utils + +data class ENV_KEY_MISSING( + val keyName: String, + override val message: String? = "$keyName was not found, please check your ENV variables" +) : Exception(message) + +data class HCTI_URL_RESPONSE_ERROR( + val response: String, + override val message: String? = "Server Error, We Recieved this Resp: $response" +) : Exception(message) + +data class RETRY_LIMIT_EXHAUSTED( + override val message: String? = "RETRY LIMIT EXHAUSTED!" +) : Exception(message) diff --git a/console-app/src/main/java/utils/Ext.kt b/console-app/src/main/java/utils/Ext.kt new file mode 100644 index 00000000..9dbb6978 --- /dev/null +++ b/console-app/src/main/java/utils/Ext.kt @@ -0,0 +1,9 @@ +package utils + +val String.byProperty: String get() = System.getenv(this) + ?: throw (ENV_KEY_MISSING(this)) + +val String.byOptionalProperty: String? get() = System.getenv(this) + +fun debug(message: String) = println("\n::debug::$message") +fun debug(tag: String, message: String) = println("\n::debug::$tag:\n$message") \ No newline at end of file diff --git a/console-app/src/main/java/utils/TestClass.kt b/console-app/src/main/java/utils/TestClass.kt new file mode 100644 index 00000000..3ffb46b9 --- /dev/null +++ b/console-app/src/main/java/utils/TestClass.kt @@ -0,0 +1,6 @@ +package utils + +import kotlinx.coroutines.runBlocking + +// Test Class- at development Time +fun main(): Unit = runBlocking {} diff --git a/settings.gradle.kts b/settings.gradle.kts index dcde5caa..a8b58618 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,7 +32,7 @@ include( ":maintenance-tasks" ) -includeBuild("mosaic") { +includeBuild("mosaic/mosaic") { dependencySubstitution { substitute(module("com.jakewharton.mosaic:mosaic-gradle-plugin")).with(project(":mosaic-gradle-plugin")) substitute(module("com.jakewharton.mosaic:mosaic-runtime")).with(project(":mosaic-runtime"))