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-isolate:$statelyIsoVersion")
|
||||
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
|
||||
implementation(Extras.youtubeDownloader)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
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 ""
|
@ -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
|
||||
|
||||
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<TrackDetails>,
|
||||
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.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()) }
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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<PlatformQueryResult,Throwable> = 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(
|
||||
|
@ -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<PlatformQueryResult, Throwable> = 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<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.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<PlatformQueryResult, Throwable> = 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
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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<String,Throwable> = getLinkFromYt1sMp3(videoID).map {
|
||||
corsApi + it
|
||||
}
|
||||
}
|
||||
|
@ -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<String, Throwable> {
|
||||
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<String,Throwable> =
|
||||
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<YoutubeTrack> {
|
||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||
|
||||
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<List<YoutubeTrack>,Throwable> =
|
||||
getYoutubeMusicResponse(query).map { youtubeResponseData ->
|
||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||
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<JsonArray>()
|
||||
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<JsonArray>()
|
||||
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<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 '
|
||||
/* 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<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
|
||||
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<String,Throwable> = 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")
|
||||
|
@ -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<PlatformQueryResult,Throwable> {
|
||||
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<PlatformQueryResult,Throwable> = 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<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 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<String>().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++
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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<HttpClient>
|
||||
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")
|
||||
|
@ -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<String,Throwable> = 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<String,Throwable> = 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<JsonObject,Throwable> = SuspendableEvent {
|
||||
httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
|
||||
body = FormDataContent(
|
||||
Parameters.build {
|
||||
append("vid", videoID)
|
||||
|
@ -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<TrackDetails>,
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
|
@ -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<String, DownloadStatus> = hashMapOf()
|
||||
// IO-Dispatcher
|
||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
// Current Platform Info
|
||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Js
|
||||
|
||||
actual suspend fun downloadTracks(
|
||||
list: List<TrackDetails>,
|
||||
fetcher: FetchPlatformQueryResult,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user