mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
Better Error Handling and Major Code Cleanup
This commit is contained in:
parent
581f4d0104
commit
0b7b93ba63
@ -44,6 +44,7 @@ kotlin {
|
|||||||
implementation("co.touchlab:stately-concurrency:$statelyVersion")
|
implementation("co.touchlab:stately-concurrency:$statelyVersion")
|
||||||
implementation("co.touchlab:stately-isolate:$statelyIsoVersion")
|
implementation("co.touchlab:stately-isolate:$statelyIsoVersion")
|
||||||
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
|
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
|
||||||
|
implementation(Extras.youtubeDownloader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
fun <T: Any> T?.requireNotNull() : T = requireNotNull(this)
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.common.models
|
|
||||||
|
|
||||||
sealed class AllPlatforms {
|
|
||||||
object Js : AllPlatforms()
|
|
||||||
object Jvm : AllPlatforms()
|
|
||||||
object Native : AllPlatforms()
|
|
||||||
}
|
|
@ -16,6 +16,9 @@
|
|||||||
|
|
||||||
package com.shabinder.common.models
|
package com.shabinder.common.models
|
||||||
|
|
||||||
|
import io.github.shabinder.TargetPlatforms
|
||||||
|
import io.github.shabinder.activePlatform
|
||||||
|
|
||||||
sealed class CorsProxy(open val url: String) {
|
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 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)
|
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.
|
* Default Self Hosted, However ask user to use extension if possible.
|
||||||
* */
|
* */
|
||||||
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
||||||
|
|
||||||
|
val corsApi get() = if (activePlatform is TargetPlatforms.Js) corsProxy.url else ""
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
package com.shabinder.common.models.event
|
||||||
|
|
||||||
|
inline fun <reified X> Event<*, *>.getAs() = when (this) {
|
||||||
|
is Event.Success -> value as? X
|
||||||
|
is Event.Failure -> error as? X
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?> Event<V, *>.success(f: (V) -> Unit) = fold(f, {})
|
||||||
|
|
||||||
|
inline fun <E : Throwable> Event<*, E>.failure(f: (E) -> Unit) = fold({}, f)
|
||||||
|
|
||||||
|
infix fun <V : Any?, E : Throwable> Event<V, E>.or(fallback: V) = when (this) {
|
||||||
|
is Event.Success -> this
|
||||||
|
else -> Event.Success(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline infix fun <V : Any?, E : Throwable> Event<V, E>.getOrElse(fallback: (E) -> V): V {
|
||||||
|
return when (this) {
|
||||||
|
is Event.Success -> value
|
||||||
|
is Event.Failure -> fallback(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V : Any?, E : Throwable> Event<V, E>.getOrNull(): V? {
|
||||||
|
return when (this) {
|
||||||
|
is Event.Success -> value
|
||||||
|
is Event.Failure -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V : Any?, E : Throwable> Event<V, E>.getThrowableOrNull(): E? {
|
||||||
|
return when (this) {
|
||||||
|
is Event.Success -> null
|
||||||
|
is Event.Failure -> error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, E : Throwable, U : Any?, F : Throwable> Event<V, E>.mapEither(
|
||||||
|
success: (V) -> U,
|
||||||
|
failure: (E) -> F
|
||||||
|
): Event<U, F> {
|
||||||
|
return when (this) {
|
||||||
|
is Event.Success -> Event.success(success(value))
|
||||||
|
is Event.Failure -> Event.error(failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, U : Any?, reified E : Throwable> Event<V, E>.map(transform: (V) -> U): Event<U, E> = 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 <V : Any?, U : Any?, reified E : Throwable> Event<V, E>.flatMap(transform: (V) -> Event<U, E>): Event<U, E> =
|
||||||
|
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 <V : Any?, E : Throwable, E2 : Throwable> Event<V, E>.mapError(transform: (E) -> E2) = when (this) {
|
||||||
|
is Event.Success -> Event.Success(value)
|
||||||
|
is Event.Failure -> Event.Failure(transform(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, E : Throwable, E2 : Throwable> Event<V, E>.flatMapError(transform: (E) -> Event<V, E2>) =
|
||||||
|
when (this) {
|
||||||
|
is Event.Success -> Event.Success(value)
|
||||||
|
is Event.Failure -> transform(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, E : Throwable> Event<V, E>.onError(f: (E) -> Unit) = when (this) {
|
||||||
|
is Event.Success -> Event.Success(value)
|
||||||
|
is Event.Failure -> {
|
||||||
|
f(error)
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, E : Throwable> Event<V, E>.onSuccess(f: (V) -> Unit): Event<V, E> {
|
||||||
|
return when (this) {
|
||||||
|
is Event.Success -> {
|
||||||
|
f(value)
|
||||||
|
this
|
||||||
|
}
|
||||||
|
is Event.Failure -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, E : Throwable> Event<V, E>.any(predicate: (V) -> Boolean): Boolean = try {
|
||||||
|
when (this) {
|
||||||
|
is Event.Success -> predicate(value)
|
||||||
|
is Event.Failure -> false
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V : Any?, U : Any?> Event<V, *>.fanout(other: () -> Event<U, *>): Event<Pair<V, U>, *> =
|
||||||
|
flatMap { outer -> other().map { outer to it } }
|
||||||
|
|
||||||
|
inline fun <V : Any?, reified E : Throwable> List<Event<V, E>>.lift(): Event<List<V>, E> = fold(
|
||||||
|
Event.success(
|
||||||
|
mutableListOf<V>()
|
||||||
|
) as Event<MutableList<V>, E>
|
||||||
|
) { acc, Event ->
|
||||||
|
acc.flatMap { combine ->
|
||||||
|
Event.map { combine.apply { add(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <V, E : Throwable> Event<V, E>.unwrap(failure: (E) -> Nothing): V =
|
||||||
|
apply { component2()?.let(failure) }.component1()!!
|
||||||
|
|
||||||
|
inline fun <V, E : Throwable> Event<V, E>.unwrapError(success: (V) -> Nothing): E =
|
||||||
|
apply { component1()?.let(success) }.component2()!!
|
||||||
|
|
||||||
|
|
||||||
|
sealed class Event<out V : Any?, out E : Throwable> {
|
||||||
|
|
||||||
|
open operator fun component1(): V? = null
|
||||||
|
open operator fun component2(): E? = null
|
||||||
|
|
||||||
|
inline fun <X> 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<out V : Any?>(val value: V) : Event<V, Nothing>() {
|
||||||
|
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<out E : Throwable>(val error: E) : Event<Nothing, E>() {
|
||||||
|
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 <E : Throwable> error(ex: E) = Failure(ex)
|
||||||
|
|
||||||
|
fun <V : Any?> success(v: V) = Success(v)
|
||||||
|
|
||||||
|
inline fun <V : Any?> of(
|
||||||
|
value: V?,
|
||||||
|
fail: (() -> Throwable) = { Throwable() }
|
||||||
|
): Event<V, Throwable> =
|
||||||
|
value?.let { success(it) } ?: error(fail())
|
||||||
|
|
||||||
|
inline fun <V : Any?, reified E : Throwable> of(crossinline f: () -> V): Event<V, E> = try {
|
||||||
|
success(f())
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
when (ex) {
|
||||||
|
is E -> error(ex)
|
||||||
|
else -> throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline operator fun <V : Any?> invoke(crossinline f: () -> V): Event<V, Throwable> = try {
|
||||||
|
success(f())
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
error(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.shabinder.common.models.event
|
||||||
|
|
||||||
|
inline fun <V> runCatching(block: () -> V): Event<V, Throwable> {
|
||||||
|
return try {
|
||||||
|
Event.success(block())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Event.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline infix fun <T, V> T.runCatching(block: T.() -> V): Event<V, Throwable> {
|
||||||
|
return try {
|
||||||
|
Event.success(block())
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Event.error(e)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.shabinder.common.models.event
|
||||||
|
|
||||||
|
class Validation<out E : Throwable>(vararg resultSequence: Event<*, E>) {
|
||||||
|
|
||||||
|
val failures: List<E> = resultSequence.filterIsInstance<Event.Failure<E>>().map { it.getThrowable() }
|
||||||
|
|
||||||
|
val hasFailure = failures.isNotEmpty()
|
||||||
|
}
|
@ -0,0 +1,159 @@
|
|||||||
|
package com.shabinder.common.models.event.coroutines
|
||||||
|
|
||||||
|
inline fun <reified X> SuspendableEvent<*, *>.getAs() = when (this) {
|
||||||
|
is SuspendableEvent.Success -> value as? X
|
||||||
|
is SuspendableEvent.Failure -> error as? X
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <V : Any?> SuspendableEvent<V, *>.success(crossinline f: suspend (V) -> Unit) = fold(f, {})
|
||||||
|
|
||||||
|
suspend inline fun <E : Throwable> SuspendableEvent<*, E>.failure(crossinline f: suspend (E) -> Unit) = fold({}, f)
|
||||||
|
|
||||||
|
infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.or(fallback: V) = when (this) {
|
||||||
|
is SuspendableEvent.Success -> this
|
||||||
|
else -> SuspendableEvent.Success(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.getOrElse(crossinline fallback:suspend (E) -> V): V {
|
||||||
|
return when (this) {
|
||||||
|
is SuspendableEvent.Success -> value
|
||||||
|
is SuspendableEvent.Failure -> fallback(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.getOrNull(): V? {
|
||||||
|
return when (this) {
|
||||||
|
is SuspendableEvent.Success -> value
|
||||||
|
is SuspendableEvent.Failure -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <V : Any?, U : Any?, E : Throwable> SuspendableEvent<V, E>.map(
|
||||||
|
crossinline transform: suspend (V) -> U
|
||||||
|
): SuspendableEvent<U, E> = 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 <V : Any?, U : Any?, E : Throwable> SuspendableEvent<V, E>.flatMap(
|
||||||
|
crossinline transform: suspend (V) -> SuspendableEvent<U, E>
|
||||||
|
): SuspendableEvent<U, E> = 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 <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.mapError(
|
||||||
|
crossinline transform: suspend (E) -> E2
|
||||||
|
) = when (this) {
|
||||||
|
is SuspendableEvent.Success -> SuspendableEvent.Success<V, E2>(value)
|
||||||
|
is SuspendableEvent.Failure -> SuspendableEvent.Failure<V, E2>(transform(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.flatMapError(
|
||||||
|
crossinline transform: suspend (E) -> SuspendableEvent<V, E2>
|
||||||
|
) = when (this) {
|
||||||
|
is SuspendableEvent.Success -> SuspendableEvent.Success(value)
|
||||||
|
is SuspendableEvent.Failure -> transform(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.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 <V : Any?, U : Any> SuspendableEvent<V, *>.fanout(
|
||||||
|
crossinline other: suspend () -> SuspendableEvent<U, *>
|
||||||
|
): SuspendableEvent<Pair<V, U>, *> =
|
||||||
|
flatMap { outer -> other().map { outer to it } }
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun <V : Any?, E : Throwable> List<SuspendableEvent<V, E>>.lift(): SuspendableEvent<List<V>, E> = fold(
|
||||||
|
SuspendableEvent.Success<MutableList<V>, E>(mutableListOf<V>()) as SuspendableEvent<MutableList<V>, E>
|
||||||
|
) { acc, result ->
|
||||||
|
acc.flatMap { combine ->
|
||||||
|
result.map { combine.apply { add(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SuspendableEvent<out V : Any?, out E : Throwable> {
|
||||||
|
|
||||||
|
abstract operator fun component1(): V?
|
||||||
|
abstract operator fun component2(): E?
|
||||||
|
|
||||||
|
suspend inline fun <X> 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<out V : Any?, out E : Throwable>(override val value: V) : SuspendableEvent<V, E>() {
|
||||||
|
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<out V : Any?, out E : Throwable>(val error: E) : SuspendableEvent<V, E>() {
|
||||||
|
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 <E : Throwable> error(ex: E) = Failure<Nothing, E>(ex)
|
||||||
|
|
||||||
|
inline fun <V : Any?> of(value: V?,crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent<V, Throwable> {
|
||||||
|
return value?.let { Success<V, Nothing>(it) } ?: error(fail())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <V : Any?, E : Throwable> of(crossinline f: suspend () -> V): SuspendableEvent<V, E> = try {
|
||||||
|
Success(f())
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Failure(ex as E)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline operator fun <V : Any?> invoke(crossinline f: suspend () -> V): SuspendableEvent<V, Throwable> = try {
|
||||||
|
Success(f())
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Failure(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.shabinder.common.models.event.coroutines
|
||||||
|
|
||||||
|
class SuspendedValidation<out E : Throwable>(vararg resultSequence: SuspendableEvent<*, E>) {
|
||||||
|
|
||||||
|
val failures: List<E> = resultSequence.filterIsInstance<SuspendableEvent.Failure<*, E>>().map { it.getThrowable() }
|
||||||
|
|
||||||
|
val hasFailure = failures.isNotEmpty()
|
||||||
|
|
||||||
|
}
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
@ -25,9 +24,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
// IO-Dispatcher
|
// IO-Dispatcher
|
||||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
// Current Platform Info
|
|
||||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
|
||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
|
@ -27,14 +27,11 @@ import com.shabinder.common.di.providers.SpotifyProvider
|
|||||||
import com.shabinder.common.di.providers.YoutubeMp3
|
import com.shabinder.common.di.providers.YoutubeMp3
|
||||||
import com.shabinder.common.di.providers.YoutubeMusic
|
import com.shabinder.common.di.providers.YoutubeMusic
|
||||||
import com.shabinder.common.di.providers.YoutubeProvider
|
import com.shabinder.common.di.providers.YoutubeProvider
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.HttpTimeout
|
import io.ktor.client.features.*
|
||||||
import io.ktor.client.features.json.JsonFeature
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
import io.ktor.client.features.json.serializer.*
|
||||||
import io.ktor.client.features.logging.DEFAULT
|
import io.ktor.client.features.logging.*
|
||||||
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
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.KoinAppDeclaration
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
@ -62,7 +59,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
|
|||||||
single { GaanaProvider(get(), get(), get()) }
|
single { GaanaProvider(get(), get(), get()) }
|
||||||
single { SaavnProvider(get(), get(), get(), get()) }
|
single { SaavnProvider(get(), get(), get(), get()) }
|
||||||
single { YoutubeProvider(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 { 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()) }
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import io.ktor.client.request.head
|
import io.ktor.client.request.*
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -34,10 +33,6 @@ expect suspend fun downloadTracks(
|
|||||||
@SharedImmutable
|
@SharedImmutable
|
||||||
expect val dispatcherIO: CoroutineDispatcher
|
expect val dispatcherIO: CoroutineDispatcher
|
||||||
|
|
||||||
// Current Platform Info
|
|
||||||
@SharedImmutable
|
|
||||||
expect val currentPlatform: AllPlatforms
|
|
||||||
|
|
||||||
suspend fun isInternetAccessible(): Boolean {
|
suspend fun isInternetAccessible(): Boolean {
|
||||||
return withContext(dispatcherIO) {
|
return withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
|
@ -16,21 +16,15 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di.gaana
|
package com.shabinder.common.di.gaana
|
||||||
|
|
||||||
import com.shabinder.common.di.currentPlatform
|
import com.shabinder.common.models.corsApi
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.corsProxy
|
|
||||||
import com.shabinder.common.models.gaana.GaanaAlbum
|
import com.shabinder.common.models.gaana.GaanaAlbum
|
||||||
import com.shabinder.common.models.gaana.GaanaArtistDetails
|
import com.shabinder.common.models.gaana.GaanaArtistDetails
|
||||||
import com.shabinder.common.models.gaana.GaanaArtistTracks
|
import com.shabinder.common.models.gaana.GaanaArtistTracks
|
||||||
import com.shabinder.common.models.gaana.GaanaPlaylist
|
import com.shabinder.common.models.gaana.GaanaPlaylist
|
||||||
import com.shabinder.common.models.gaana.GaanaSong
|
import com.shabinder.common.models.gaana.GaanaSong
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.get
|
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 const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
|
||||||
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
|
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
|
||||||
|
@ -22,10 +22,12 @@ import com.shabinder.common.di.finalOutputDir
|
|||||||
import com.shabinder.common.di.gaana.GaanaRequests
|
import com.shabinder.common.di.gaana.GaanaRequests
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
import com.shabinder.common.models.TrackDetails
|
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.gaana.GaanaTrack
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
|
|
||||||
class GaanaProvider(
|
class GaanaProvider(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
@ -35,7 +37,7 @@ class GaanaProvider(
|
|||||||
|
|
||||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult? {
|
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
|
||||||
// Link Schema: https://gaana.com/type/link
|
// Link Schema: https://gaana.com/type/link
|
||||||
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
||||||
|
|
||||||
@ -44,17 +46,13 @@ class GaanaProvider(
|
|||||||
|
|
||||||
// Error
|
// Error
|
||||||
if (type == "Error" || link == "Error") {
|
if (type == "Error" || link == "Error") {
|
||||||
return null
|
throw SpotiFlyerException.LinkInvalid()
|
||||||
}
|
|
||||||
return try {
|
|
||||||
gaanaSearch(
|
|
||||||
type,
|
|
||||||
link
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gaanaSearch(
|
||||||
|
type,
|
||||||
|
link
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun gaanaSearch(
|
private suspend fun gaanaSearch(
|
||||||
@ -137,6 +135,7 @@ class GaanaProvider(
|
|||||||
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
|
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
|
||||||
return if (dir.isPresent(
|
return if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
|
@ -8,10 +8,12 @@ import com.shabinder.common.di.saavn.JioSaavnRequests
|
|||||||
import com.shabinder.common.di.utils.removeIllegalChars
|
import com.shabinder.common.di.utils.removeIllegalChars
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
import com.shabinder.common.models.TrackDetails
|
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.saavn.SaavnSong
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
|
|
||||||
class SaavnProvider(
|
class SaavnProvider(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
@ -20,16 +22,15 @@ class SaavnProvider(
|
|||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
) : JioSaavnRequests {
|
) : JioSaavnRequests {
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult {
|
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
|
||||||
val result = PlatformQueryResult(
|
PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
title = "",
|
title = "",
|
||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.JioSaavn
|
Source.JioSaavn
|
||||||
)
|
).apply {
|
||||||
with(result) {
|
|
||||||
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
|
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
|
||||||
"song" -> {
|
"song" -> {
|
||||||
getSong(fullLink).let {
|
getSong(fullLink).let {
|
||||||
@ -59,12 +60,10 @@ class SaavnProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Handle Error
|
throw SpotiFlyerException.LinkInvalid(fullLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<SaavnSong>.toTrackDetails(type: String, subFolder: String): List<TrackDetails> = this.map {
|
private fun List<SaavnSong>.toTrackDetails(type: String, subFolder: String): List<TrackDetails> = this.map {
|
||||||
|
@ -27,17 +27,19 @@ import com.shabinder.common.di.spotify.authenticateSpotify
|
|||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.NativeAtomicReference
|
import com.shabinder.common.models.NativeAtomicReference
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
import com.shabinder.common.models.TrackDetails
|
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.Album
|
||||||
import com.shabinder.common.models.spotify.Image
|
import com.shabinder.common.models.spotify.Image
|
||||||
import com.shabinder.common.models.spotify.PlaylistTrack
|
import com.shabinder.common.models.spotify.PlaylistTrack
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import com.shabinder.common.models.spotify.Track
|
import com.shabinder.common.models.spotify.Track
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.defaultRequest
|
import io.ktor.client.features.*
|
||||||
import io.ktor.client.features.json.JsonFeature
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
import io.ktor.client.features.json.serializer.*
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
class SpotifyProvider(
|
class SpotifyProvider(
|
||||||
private val tokenStore: TokenStore,
|
private val tokenStore: TokenStore,
|
||||||
@ -64,7 +66,7 @@ class SpotifyProvider(
|
|||||||
|
|
||||||
override val httpClientRef = NativeAtomicReference(createHttpClient(true))
|
override val httpClientRef = NativeAtomicReference(createHttpClient(true))
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult? {
|
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
|
||||||
|
|
||||||
var spotifyLink =
|
var spotifyLink =
|
||||||
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
|
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
|
||||||
@ -78,15 +80,16 @@ class SpotifyProvider(
|
|||||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||||
|
|
||||||
if (type == "Error" || link == "Error") {
|
if (type == "Error" || link == "Error") {
|
||||||
return null
|
throw SpotiFlyerException.LinkInvalid(fullLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == "episode" || type == "show") {
|
if (type == "episode" || type == "show") {
|
||||||
// TODO Implementation
|
throw SpotiFlyerException.FeatureNotImplementedYet(
|
||||||
return null
|
"Support for Spotify's ${type.uppercase()} isn't implemented yet"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
try {
|
||||||
spotifySearch(
|
spotifySearch(
|
||||||
type,
|
type,
|
||||||
link
|
link
|
||||||
@ -95,16 +98,11 @@ class SpotifyProvider(
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
|
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
|
||||||
authenticateSpotifyClient(true)
|
authenticateSpotifyClient(true)
|
||||||
// Retry Search
|
|
||||||
try {
|
spotifySearch(
|
||||||
spotifySearch(
|
type,
|
||||||
type,
|
link
|
||||||
link
|
)
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,15 +110,14 @@ class SpotifyProvider(
|
|||||||
type: String,
|
type: String,
|
||||||
link: String
|
link: String
|
||||||
): PlatformQueryResult {
|
): PlatformQueryResult {
|
||||||
val result = PlatformQueryResult(
|
return PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
title = "",
|
title = "",
|
||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.Spotify
|
Source.Spotify
|
||||||
)
|
).apply {
|
||||||
with(result) {
|
|
||||||
when (type) {
|
when (type) {
|
||||||
"track" -> {
|
"track" -> {
|
||||||
getTrack(link).also {
|
getTrack(link).also {
|
||||||
@ -190,11 +187,10 @@ class SpotifyProvider(
|
|||||||
"show" -> { // TODO
|
"show" -> { // TODO
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// TODO Handle Error
|
throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -17,28 +17,28 @@
|
|||||||
package com.shabinder.common.di.providers
|
package com.shabinder.common.di.providers
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
||||||
import com.shabinder.common.di.currentPlatform
|
|
||||||
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
|
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.*
|
import io.ktor.client.*
|
||||||
|
|
||||||
class YoutubeMp3(
|
interface YoutubeMp3: Yt1sMp3 {
|
||||||
override val httpClient: HttpClient,
|
|
||||||
override val logger: Kermit,
|
companion object {
|
||||||
private val dir: Dir,
|
operator fun invoke(
|
||||||
) : Yt1sMp3 {
|
client: HttpClient,
|
||||||
suspend fun getMp3DownloadLink(videoID: String): String? = try {
|
logger: Kermit
|
||||||
logger.i { "Youtube MP3 Link Fetching!" }
|
): AudioToMp3 {
|
||||||
getLinkFromYt1sMp3(videoID)?.let {
|
return object : AudioToMp3 {
|
||||||
logger.i { "Download Link: $it" }
|
override val client: HttpClient = client
|
||||||
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
|
override val logger: Kermit = logger
|
||||||
"https://cors.spotiflyer.ml/cors/$it"
|
}
|
||||||
// "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue
|
|
||||||
else it
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
}
|
||||||
e.printStackTrace()
|
|
||||||
null
|
suspend fun getMp3DownloadLink(videoID: String): SuspendableEvent<String,Throwable> = getLinkFromYt1sMp3(videoID).map {
|
||||||
|
corsApi + it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,18 @@ package com.shabinder.common.di.providers
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
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.TrackDetails
|
||||||
import com.shabinder.common.models.YoutubeTrack
|
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.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.headers
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.post
|
import io.ktor.http.*
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
@ -37,6 +40,7 @@ import kotlinx.serialization.json.jsonObject
|
|||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import kotlin.collections.set
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class YoutubeMusic constructor(
|
class YoutubeMusic constructor(
|
||||||
@ -54,179 +58,178 @@ class YoutubeMusic constructor(
|
|||||||
|
|
||||||
suspend fun findSongDownloadURL(
|
suspend fun findSongDownloadURL(
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
): String? {
|
): SuspendableEvent<String, Throwable> {
|
||||||
val bestMatchVideoID = getYTIDBestMatch(trackDetails)
|
val bestMatchVideoID = getYTIDBestMatch(trackDetails)
|
||||||
return bestMatchVideoID?.let { videoID ->
|
return bestMatchVideoID.flatMap { videoID ->
|
||||||
youtubeMp3.getMp3DownloadLink(videoID) ?: youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url?.let { m4aLink ->
|
// Get Downloadable Link
|
||||||
audioToMp3.convertToMp3(
|
youtubeMp3.getMp3DownloadLink(videoID).flatMapError {
|
||||||
m4aLink
|
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
|
trackDetails: TrackDetails
|
||||||
): String? {
|
):SuspendableEvent<String,Throwable> =
|
||||||
return try {
|
getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList ->
|
||||||
sortByBestMatch(
|
sortByBestMatch(
|
||||||
getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}"),
|
matchList,
|
||||||
trackName = trackDetails.title,
|
trackName = trackDetails.title,
|
||||||
trackArtists = trackDetails.artists,
|
trackArtists = trackDetails.artists,
|
||||||
trackDurationSec = trackDetails.durationSec
|
trackDurationSec = trackDetails.durationSec
|
||||||
).keys.firstOrNull()
|
).keys.firstOrNull() ?: throw SpotiFlyerException.NoMatchFound(trackDetails.title)
|
||||||
} catch (e: Exception) {
|
|
||||||
// All Internet/Client Related Errors
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
private suspend fun getYTTracks(query: String): List<YoutubeTrack> {
|
|
||||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
|
||||||
|
|
||||||
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
|
private suspend fun getYTTracks(query: String): SuspendableEvent<List<YoutubeTrack>,Throwable> =
|
||||||
logger.i { "Youtube Music Response Recieved" }
|
getYoutubeMusicResponse(query).map { youtubeResponseData ->
|
||||||
val contentBlocks = responseObj.jsonObject["contents"]
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
?.jsonObject?.get("sectionListRenderer")
|
val responseObj = Json.parseToJsonElement(youtubeResponseData)
|
||||||
?.jsonObject?.get("contents")?.jsonArray
|
// logger.i { "Youtube Music Response Received" }
|
||||||
|
val contentBlocks = responseObj.jsonObject["contents"]
|
||||||
|
?.jsonObject?.get("sectionListRenderer")
|
||||||
|
?.jsonObject?.get("contents")?.jsonArray
|
||||||
|
|
||||||
val resultBlocks = mutableListOf<JsonArray>()
|
val resultBlocks = mutableListOf<JsonArray>()
|
||||||
if (contentBlocks != null) {
|
if (contentBlocks != null) {
|
||||||
for (cBlock in contentBlocks) {
|
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()
|
|
||||||
) {
|
|
||||||
/**
|
/**
|
||||||
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
*Ignore user-suggestion
|
||||||
* I have no clue what they are and why there even exist
|
*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
|
||||||
if(!contents.containsKey("overlay")){
|
*loop below if throw a keyError if we don't ignore them
|
||||||
println(contents)
|
*/
|
||||||
continue
|
if (cBlock.jsonObject.containsKey("itemSectionRenderer")) {
|
||||||
TODO check and correct
|
continue
|
||||||
}*/
|
}
|
||||||
|
|
||||||
val result = contents.jsonObject["musicResponsiveListItemRenderer"]
|
for (
|
||||||
?.jsonObject?.get("flexColumns")?.jsonArray
|
contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray
|
||||||
|
?: listOf()
|
||||||
// Add the linkBlock
|
) {
|
||||||
val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"]
|
/**
|
||||||
?.jsonObject?.get("overlay")
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
?.jsonObject?.get("musicItemThumbnailOverlayRenderer")
|
* I have no clue what they are and why there even exist
|
||||||
?.jsonObject?.get("content")
|
*
|
||||||
?.jsonObject?.get("musicPlayButtonRenderer")
|
if(!contents.containsKey("overlay")){
|
||||||
?.jsonObject?.get("playNavigationEndpoint")
|
println(contents)
|
||||||
|
continue
|
||||||
// detailsBlock is always a list, so we just append the linkBlock to it
|
TODO check and correct
|
||||||
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
}*/
|
||||||
val finalResult = buildJsonArray {
|
|
||||||
result?.let { add(it) }
|
val result = contents.jsonObject["musicResponsiveListItemRenderer"]
|
||||||
linkBlock?.let { add(it) }
|
?.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
|
/* 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
|
! 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
|
! 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:
|
! relevant details. What you need to know to understand how we do that here:
|
||||||
!
|
!
|
||||||
! Songs details are ALWAYS in the following order:
|
! Songs details are ALWAYS in the following order:
|
||||||
! 0 - Name
|
! 0 - Name
|
||||||
! 1 - Type (Song)
|
! 1 - Type (Song)
|
||||||
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
||||||
! 3 - Album
|
! 3 - Album
|
||||||
! 4 - Duration (mm:ss)
|
! 4 - Duration (mm:ss)
|
||||||
!
|
!
|
||||||
! Video details are ALWAYS in the following order:
|
! Video details are ALWAYS in the following order:
|
||||||
! 0 - Name
|
! 0 - Name
|
||||||
! 1 - Type (Video)
|
! 1 - Type (Video)
|
||||||
! 2 - Channel
|
! 2 - Channel
|
||||||
! 3 - Viewers
|
! 3 - Viewers
|
||||||
! 4 - Duration (hh:mm:ss)
|
! 4 - Duration (hh:mm:ss)
|
||||||
!
|
!
|
||||||
! We blindly gather all the details we get our hands on, then
|
! We blindly gather all the details we get our hands on, then
|
||||||
! cherry pick the details we need based on their index numbers,
|
! cherry pick the details we need based on their index numbers,
|
||||||
! we do so only if their Type is 'Song' or 'Video
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
*/
|
|
||||||
|
|
||||||
for (result in resultBlocks) {
|
|
||||||
|
|
||||||
// Blindly gather available details
|
|
||||||
val availableDetails = mutableListOf<String>()
|
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
for (result in resultBlocks) {
|
||||||
val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]
|
|
||||||
?.jsonObject?.get("text")
|
|
||||||
?.jsonObject?.get("runs")?.jsonArray ?: listOf()
|
|
||||||
|
|
||||||
for (d in details) {
|
// Blindly gather available details
|
||||||
d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let {
|
val availableDetails = mutableListOf<String>()
|
||||||
if (it != " • ") {
|
|
||||||
availableDetails.add(it)
|
/*
|
||||||
|
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()}
|
||||||
// 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
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
! grab Video ID
|
! Filter Out non-Song/Video results and incomplete results here itself
|
||||||
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
! From what we know about detail order, note that [1] - indicate result type
|
||||||
! 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
|
|
||||||
*/
|
*/
|
||||||
|
if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) {
|
||||||
|
|
||||||
val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
val ytTrack = YoutubeTrack(
|
if (availableDetails[4].split(':').size != 2) continue
|
||||||
name = availableDetails[0],
|
|
||||||
type = availableDetails[1],
|
/*
|
||||||
artist = availableDetails[2],
|
! grab Video ID
|
||||||
duration = availableDetails[4],
|
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||||
videoId = videoId
|
! 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
|
||||||
youtubeTracks.add(ytTrack)
|
! 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")}
|
// logger.d {youtubeTracks.joinToString("\n")}
|
||||||
return youtubeTracks
|
youtubeTracks
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortByBestMatch(
|
private fun sortByBestMatch(
|
||||||
@ -246,8 +249,8 @@ class YoutubeMusic constructor(
|
|||||||
// most song results on youtube go by $artist - $songName or artist1/artist2
|
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||||
var hasCommonWord = false
|
var hasCommonWord = false
|
||||||
|
|
||||||
val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: ""
|
val resultName = result.name?.lowercase()?.replace("-", " ")?.replace("/", " ") ?: ""
|
||||||
val trackNameWords = trackName.toLowerCase().split(" ")
|
val trackNameWords = trackName.lowercase().split(" ")
|
||||||
|
|
||||||
for (nameWord in trackNameWords) {
|
for (nameWord in trackNameWords) {
|
||||||
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
|
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
|
||||||
@ -266,12 +269,12 @@ class YoutubeMusic constructor(
|
|||||||
|
|
||||||
if (result.type == "Song") {
|
if (result.type == "Song") {
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if (FuzzySearch.ratio(artist.toLowerCase(), result.artist?.toLowerCase() ?: "") > 85)
|
if (FuzzySearch.ratio(artist.lowercase(), result.artist?.lowercase() ?: "") > 85)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
}
|
}
|
||||||
} else { // i.e. is a Video
|
} else { // i.e. is a Video
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if (FuzzySearch.partialRatio(artist.toLowerCase(), result.name?.toLowerCase() ?: "") > 85)
|
if (FuzzySearch.partialRatio(artist.lowercase(), result.name?.lowercase() ?: "") > 85)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,9 +306,8 @@ class YoutubeMusic constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getYoutubeMusicResponse(query: String): String {
|
private suspend fun getYoutubeMusicResponse(query: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
logger.i { "Fetching Youtube Music Response" }
|
httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
|
||||||
return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
|
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
headers {
|
headers {
|
||||||
append("referer", "https://music.youtube.com/search")
|
append("referer", "https://music.youtube.com/search")
|
||||||
|
@ -22,7 +22,9 @@ import com.shabinder.common.di.finalOutputDir
|
|||||||
import com.shabinder.common.di.utils.removeIllegalChars
|
import com.shabinder.common.di.utils.removeIllegalChars
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.github.shabinder.YoutubeDownloader
|
import io.github.shabinder.YoutubeDownloader
|
||||||
import io.github.shabinder.models.YoutubeVideo
|
import io.github.shabinder.models.YoutubeVideo
|
||||||
@ -49,7 +51,7 @@ class YoutubeProvider(
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
private val sampleDomain3 = "youtu.be"
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult? {
|
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult,Throwable> {
|
||||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if (link.contains("playlist", true) || link.contains("list", true)) {
|
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
@ -77,74 +79,15 @@ class YoutubeProvider(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
logger.d { "Your Youtube Link is not of a Video!!" }
|
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||||
null
|
SuspendableEvent.error(SpotiFlyerException.LinkInvalid(fullLink))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getYTPlaylist(
|
private suspend fun getYTPlaylist(
|
||||||
searchId: String
|
searchId: String
|
||||||
): PlatformQueryResult? {
|
): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
|
||||||
val result = PlatformQueryResult(
|
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(
|
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
title = "",
|
title = "",
|
||||||
@ -152,47 +95,90 @@ class YoutubeProvider(
|
|||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
).apply {
|
).apply {
|
||||||
try {
|
val playlist = ytDownloader.getPlaylist(searchId)
|
||||||
logger.i { searchId }
|
val playlistDetails = playlist.details
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val name = playlistDetails.title
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
subFolder = removeIllegalChars(name)
|
||||||
val detail = video.videoDetails
|
val videos = playlist.videos
|
||||||
val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true)
|
|
||||||
?: detail.title ?: ""
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
// logger.i{ detail.toString() }
|
videos.firstOrNull()?.videoId
|
||||||
trackList = listOf(
|
}/hqdefault.jpg"
|
||||||
TrackDetails(
|
title = name
|
||||||
title = name,
|
|
||||||
artists = listOf(detail.author ?: "N/A"),
|
trackList = videos.map {
|
||||||
durationSec = detail.lengthSeconds,
|
TrackDetails(
|
||||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
title = it.title ?: "N/A",
|
||||||
source = Source.YouTube,
|
artists = listOf(it.author ?: "N/A"),
|
||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
durationSec = it.lengthSeconds,
|
||||||
downloaded = if (dir.isPresent(
|
albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
|
||||||
dir.finalOutputDir(
|
source = Source.YouTube,
|
||||||
itemName = name,
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
||||||
type = folderType,
|
downloaded = if (dir.isPresent(
|
||||||
subFolder = subFolder,
|
dir.finalOutputDir(
|
||||||
defaultDir = dir.defaultDir()
|
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<PlatformQueryResult,Throwable> = 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ package com.shabinder.common.di.saavn
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
||||||
import com.shabinder.common.di.gaana.corsApi
|
|
||||||
import com.shabinder.common.di.globalJson
|
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.SaavnAlbum
|
||||||
import com.shabinder.common.models.saavn.SaavnPlaylist
|
import com.shabinder.common.models.saavn.SaavnPlaylist
|
||||||
import com.shabinder.common.models.saavn.SaavnSearchResult
|
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.getJsonArray
|
||||||
import io.github.shabinder.utils.getJsonObject
|
import io.github.shabinder.utils.getJsonObject
|
||||||
import io.github.shabinder.utils.getString
|
import io.github.shabinder.utils.getString
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.ServerResponseException
|
import io.ktor.client.features.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
@ -24,6 +24,7 @@ import kotlinx.serialization.json.buildJsonArray
|
|||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
interface JioSaavnRequests {
|
interface JioSaavnRequests {
|
||||||
|
|
||||||
@ -237,8 +238,8 @@ interface JioSaavnRequests {
|
|||||||
for (result in tracks) {
|
for (result in tracks) {
|
||||||
var hasCommonWord = false
|
var hasCommonWord = false
|
||||||
|
|
||||||
val resultName = result.title.toLowerCase().replace("/", " ")
|
val resultName = result.title.lowercase().replace("/", " ")
|
||||||
val trackNameWords = trackName.toLowerCase().split(" ")
|
val trackNameWords = trackName.lowercase().split(" ")
|
||||||
|
|
||||||
for (nameWord in trackNameWords) {
|
for (nameWord in trackNameWords) {
|
||||||
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
|
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
|
// String Containing All Artist Names from JioSaavn Search Result
|
||||||
val artistListString = mutableSetOf<String>().apply {
|
val artistListString = mutableSetOf<String>().apply {
|
||||||
result.more_info?.singers?.split(",")?.let { addAll(it) }
|
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(" , ")
|
}.joinToString(" , ")
|
||||||
|
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
|
if (FuzzySearch.partialRatio(artist.lowercase(), artistListString) > 85)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ package com.shabinder.common.di.spotify
|
|||||||
import com.shabinder.common.di.globalJson
|
import com.shabinder.common.di.globalJson
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.common.models.spotify.TokenData
|
import com.shabinder.common.models.spotify.TokenData
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.auth.Auth
|
import io.ktor.client.features.auth.*
|
||||||
import io.ktor.client.features.auth.providers.basic
|
import io.ktor.client.features.auth.providers.*
|
||||||
import io.ktor.client.features.json.JsonFeature
|
import io.ktor.client.features.json.*
|
||||||
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
import io.ktor.client.features.json.serializer.*
|
||||||
import io.ktor.client.request.forms.FormDataContent
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.forms.*
|
||||||
import io.ktor.http.Parameters
|
import io.ktor.http.*
|
||||||
import kotlin.native.concurrent.SharedImmutable
|
import kotlin.native.concurrent.SharedImmutable
|
||||||
|
|
||||||
suspend fun authenticateSpotify(): TokenData? {
|
suspend fun authenticateSpotify(): TokenData? {
|
||||||
@ -48,9 +48,10 @@ private val spotifyAuthClient by lazy {
|
|||||||
|
|
||||||
install(Auth) {
|
install(Auth) {
|
||||||
basic {
|
basic {
|
||||||
sendWithoutRequest = true
|
sendWithoutRequest { true }
|
||||||
username = clientId
|
credentials {
|
||||||
password = clientSecret
|
BasicAuthCredentials(clientId, clientSecret)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
|
@ -16,14 +16,16 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di.spotify
|
package com.shabinder.common.di.spotify
|
||||||
|
|
||||||
import com.shabinder.common.di.gaana.corsApi
|
|
||||||
import com.shabinder.common.models.NativeAtomicReference
|
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.Album
|
||||||
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
|
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
|
||||||
import com.shabinder.common.models.spotify.Playlist
|
import com.shabinder.common.models.spotify.Playlist
|
||||||
import com.shabinder.common.models.spotify.Track
|
import com.shabinder.common.models.spotify.Track
|
||||||
import io.ktor.client.HttpClient
|
import io.github.shabinder.TargetPlatforms
|
||||||
import io.ktor.client.request.get
|
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"
|
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
|
||||||
|
|
||||||
@ -32,7 +34,7 @@ interface SpotifyRequests {
|
|||||||
val httpClientRef: NativeAtomicReference<HttpClient>
|
val httpClientRef: NativeAtomicReference<HttpClient>
|
||||||
val httpClient: HttpClient get() = httpClientRef.value
|
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 {
|
suspend fun getPlaylist(playlistID: String): Playlist {
|
||||||
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
||||||
|
@ -17,11 +17,13 @@
|
|||||||
package com.shabinder.common.di.youtubeMp3
|
package com.shabinder.common.di.youtubeMp3
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.gaana.corsApi
|
import com.shabinder.common.models.corsApi
|
||||||
import io.ktor.client.HttpClient
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import io.ktor.client.request.forms.FormDataContent
|
import com.shabinder.common.requireNotNull
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.*
|
||||||
import io.ktor.http.Parameters
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.http.*
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
@ -33,18 +35,24 @@ interface Yt1sMp3 {
|
|||||||
|
|
||||||
val httpClient: HttpClient
|
val httpClient: HttpClient
|
||||||
val logger: Kermit
|
val logger: Kermit
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Downloadable Mp3 Link for YT videoID.
|
* Downloadable Mp3 Link for YT videoID.
|
||||||
* */
|
* */
|
||||||
suspend fun getLinkFromYt1sMp3(videoID: String): String? =
|
suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
getConvertedMp3Link(videoID, getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "")
|
getConvertedMp3Link(
|
||||||
|
videoID,
|
||||||
|
getKey(videoID).value
|
||||||
|
).value["dlink"].requireNotNull()
|
||||||
|
.jsonPrimitive.content.replace("\"", "")
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* POST:https://yt1s.com/api/ajaxSearch/index
|
* POST:https://yt1s.com/api/ajaxSearch/index
|
||||||
* Body Form= q:yt video link ,vt:format=mp3
|
* Body Form= q:yt video link ,vt:format=mp3
|
||||||
* */
|
* */
|
||||||
private suspend fun getKey(videoID: String): String {
|
private suspend fun getKey(videoID: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
val response: JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
|
val response: JsonObject = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
|
||||||
body = FormDataContent(
|
body = FormDataContent(
|
||||||
Parameters.build {
|
Parameters.build {
|
||||||
append("q", "https://www.youtube.com/watch?v=$videoID")
|
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? {
|
private suspend fun getConvertedMp3Link(videoID: String, key: String): SuspendableEvent<JsonObject,Throwable> = SuspendableEvent {
|
||||||
return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
|
httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
|
||||||
body = FormDataContent(
|
body = FormDataContent(
|
||||||
Parameters.build {
|
Parameters.build {
|
||||||
append("vid", videoID)
|
append("vid", videoID)
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import com.shabinder.common.di.utils.ParallelExecutor
|
import com.shabinder.common.di.utils.ParallelExecutor
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
@ -34,9 +33,6 @@ val DownloadScope = ParallelExecutor(Dispatchers.IO)
|
|||||||
// IO-Dispatcher
|
// IO-Dispatcher
|
||||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
|
||||||
|
|
||||||
// Current Platform Info
|
|
||||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
|
||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
@ -34,9 +33,6 @@ val allTracksStatus: HashMap<String, DownloadStatus> = hashMapOf()
|
|||||||
// IO-Dispatcher
|
// IO-Dispatcher
|
||||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
||||||
|
|
||||||
// Current Platform Info
|
|
||||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Js
|
|
||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
|
@ -28,12 +28,10 @@ import com.arkivanov.decompose.statekeeper.Parcelable
|
|||||||
import com.arkivanov.decompose.statekeeper.Parcelize
|
import com.arkivanov.decompose.statekeeper.Parcelize
|
||||||
import com.arkivanov.decompose.value.Value
|
import com.arkivanov.decompose.value.Value
|
||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.common.di.currentPlatform
|
|
||||||
import com.shabinder.common.di.providers.SpotifyProvider
|
import com.shabinder.common.di.providers.SpotifyProvider
|
||||||
import com.shabinder.common.list.SpotiFlyerList
|
import com.shabinder.common.list.SpotiFlyerList
|
||||||
import com.shabinder.common.main.SpotiFlyerMain
|
import com.shabinder.common.main.SpotiFlyerMain
|
||||||
import com.shabinder.common.models.Actions
|
import com.shabinder.common.models.Actions
|
||||||
import com.shabinder.common.models.AllPlatforms
|
|
||||||
import com.shabinder.common.models.Consumer
|
import com.shabinder.common.models.Consumer
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot
|
import com.shabinder.common.root.SpotiFlyerRoot
|
||||||
@ -80,10 +78,7 @@ internal class SpotiFlyerRootImpl(
|
|||||||
instanceKeeper.ensureNeverFrozen()
|
instanceKeeper.ensureNeverFrozen()
|
||||||
methods.value = dependencies.actions.freeze()
|
methods.value = dependencies.actions.freeze()
|
||||||
/*Authenticate Spotify Client*/
|
/*Authenticate Spotify Client*/
|
||||||
authenticateSpotify(
|
authenticateSpotify(dependencies.fetchPlatformQueryResult.spotifyProvider)
|
||||||
dependencies.fetchPlatformQueryResult.spotifyProvider,
|
|
||||||
currentPlatform is AllPlatforms.Js
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val router =
|
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) {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
analytics.appLaunchEvent()
|
analytics.appLaunchEvent()
|
||||||
/*Authenticate Spotify Client*/
|
/*Authenticate Spotify Client*/
|
||||||
spotifyProvider.authenticateSpotifyClient(override)
|
spotifyProvider.authenticateSpotifyClient()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user