Better Error Handling and Major Code Cleanup

This commit is contained in:
shabinder 2021-06-21 00:44:47 +05:30
parent 581f4d0104
commit 0b7b93ba63
27 changed files with 786 additions and 420 deletions

View File

@ -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 {

View File

@ -0,0 +1,3 @@
package com.shabinder.common
fun <T: Any> T?.requireNotNull() : T = requireNotNull(this)

View File

@ -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()
}

View File

@ -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 ""

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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,

View File

@ -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()) }
} }

View File

@ -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 {

View File

@ -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"

View File

@ -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(

View File

@ -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 {

View File

@ -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
} }
/* /*

View File

@ -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
} }
} }

View File

@ -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")

View File

@ -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
}
} }
} }

View File

@ -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++
} }

View File

@ -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) {

View File

@ -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")

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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()
} }
} }