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-isolate:$statelyIsoVersion")
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
implementation(Extras.youtubeDownloader)
}
}
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
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 ""

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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