mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
Error Handling WIP
This commit is contained in:
parent
0b7b93ba63
commit
8d5e9cdccc
@ -3,6 +3,7 @@ package com.shabinder.common.models
|
|||||||
sealed class SpotiFlyerException(override val message: String): Exception(message) {
|
sealed class SpotiFlyerException(override val message: String): Exception(message) {
|
||||||
|
|
||||||
data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message)
|
data class FeatureNotImplementedYet(override val message: String = "Feature not yet implemented."): SpotiFlyerException(message)
|
||||||
|
data class NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message)
|
||||||
|
|
||||||
data class NoMatchFound(
|
data class NoMatchFound(
|
||||||
val trackName: String? = null,
|
val trackName: String? = null,
|
||||||
@ -14,6 +15,15 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag
|
|||||||
override val message: String = "No Downloadable link found for videoID: $videoID"
|
override val message: String = "No Downloadable link found for videoID: $videoID"
|
||||||
): SpotiFlyerException(message)
|
): SpotiFlyerException(message)
|
||||||
|
|
||||||
|
data class DownloadLinkFetchFailed(
|
||||||
|
val trackName: String,
|
||||||
|
val jioSaavnError: Throwable,
|
||||||
|
val ytMusicError: Throwable,
|
||||||
|
override val message: String = "No Downloadable link found for track: $trackName," +
|
||||||
|
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n " +
|
||||||
|
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n "
|
||||||
|
): SpotiFlyerException(message)
|
||||||
|
|
||||||
data class LinkInvalid(
|
data class LinkInvalid(
|
||||||
val link: String? = null,
|
val link: String? = null,
|
||||||
override val message: String = "Entered Link is NOT Valid!\n ${link ?: ""}"
|
override val message: String = "Entered Link is NOT Valid!\n ${link ?: ""}"
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package com.shabinder.common.models.event
|
package com.shabinder.common.models.event
|
||||||
|
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
inline fun <reified X> Event<*, *>.getAs() = when (this) {
|
inline fun <reified X> Event<*, *>.getAs() = when (this) {
|
||||||
is Event.Success -> value as? X
|
is Event.Success -> value as? X
|
||||||
is Event.Failure -> error as? X
|
is Event.Failure -> error as? X
|
||||||
@ -128,7 +131,7 @@ inline fun <V, E : Throwable> Event<V, E>.unwrapError(success: (V) -> Nothing):
|
|||||||
apply { component1()?.let(success) }.component2()!!
|
apply { component1()?.let(success) }.component2()!!
|
||||||
|
|
||||||
|
|
||||||
sealed class Event<out V : Any?, out E : Throwable> {
|
sealed class Event<out V : Any?, out E : Throwable>: ReadOnlyProperty<Any?, V> {
|
||||||
|
|
||||||
open operator fun component1(): V? = null
|
open operator fun component1(): V? = null
|
||||||
open operator fun component2(): E? = null
|
open operator fun component2(): E? = null
|
||||||
@ -138,13 +141,11 @@ sealed class Event<out V : Any?, out E : Throwable> {
|
|||||||
is Failure -> failure(this.error)
|
is Failure -> failure(this.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun get(): V
|
abstract val value: V
|
||||||
|
|
||||||
class Success<out V : Any?>(val value: V) : Event<V, Nothing>() {
|
class Success<out V : Any?>(override val value: V) : Event<V, Nothing>() {
|
||||||
override fun component1(): V? = value
|
override fun component1(): V? = value
|
||||||
|
|
||||||
override fun get(): V = value
|
|
||||||
|
|
||||||
override fun toString() = "[Success: $value]"
|
override fun toString() = "[Success: $value]"
|
||||||
|
|
||||||
override fun hashCode(): Int = value.hashCode()
|
override fun hashCode(): Int = value.hashCode()
|
||||||
@ -153,12 +154,14 @@ sealed class Event<out V : Any?, out E : Throwable> {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is Success<*> && value == other.value
|
return other is Success<*> && value == other.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
|
||||||
}
|
}
|
||||||
|
|
||||||
class Failure<out E : Throwable>(val error: E) : Event<Nothing, E>() {
|
class Failure<out E : Throwable>(val error: E) : Event<Nothing, E>() {
|
||||||
override fun component2(): E? = error
|
override fun component2(): E = error
|
||||||
|
|
||||||
override fun get() = throw error
|
override val value: Nothing = throw error
|
||||||
|
|
||||||
fun getThrowable(): E = error
|
fun getThrowable(): E = error
|
||||||
|
|
||||||
@ -170,6 +173,8 @@ sealed class Event<out V : Any?, out E : Throwable> {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is Failure<*> && error == other.error
|
return other is Failure<*> && error == other.error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): Nothing = value
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package com.shabinder.common.models.event.coroutines
|
package com.shabinder.common.models.event.coroutines
|
||||||
|
|
||||||
|
import kotlin.properties.ReadOnlyProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
inline fun <reified X> SuspendableEvent<*, *>.getAs() = when (this) {
|
inline fun <reified X> SuspendableEvent<*, *>.getAs() = when (this) {
|
||||||
is SuspendableEvent.Success -> value as? X
|
is SuspendableEvent.Success -> value as? X
|
||||||
is SuspendableEvent.Failure -> error as? X
|
is SuspendableEvent.Failure -> error as? X
|
||||||
@ -52,16 +55,24 @@ suspend inline fun <V : Any?, U : Any?, E : Throwable> SuspendableEvent<V, E>.fl
|
|||||||
|
|
||||||
suspend inline fun <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.mapError(
|
suspend inline fun <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.mapError(
|
||||||
crossinline transform: suspend (E) -> E2
|
crossinline transform: suspend (E) -> E2
|
||||||
) = when (this) {
|
) = try {
|
||||||
is SuspendableEvent.Success -> SuspendableEvent.Success<V, E2>(value)
|
when (this) {
|
||||||
is SuspendableEvent.Failure -> SuspendableEvent.Failure<V, E2>(transform(error))
|
is SuspendableEvent.Success -> SuspendableEvent.Success<V, E2>(value)
|
||||||
|
is SuspendableEvent.Failure -> SuspendableEvent.Failure<V, E2>(transform(error))
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
SuspendableEvent.error(ex as E)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.flatMapError(
|
suspend inline fun <V : Any?, E : Throwable, E2 : Throwable> SuspendableEvent<V, E>.flatMapError(
|
||||||
crossinline transform: suspend (E) -> SuspendableEvent<V, E2>
|
crossinline transform: suspend (E) -> SuspendableEvent<V, E2>
|
||||||
) = when (this) {
|
) = try {
|
||||||
is SuspendableEvent.Success -> SuspendableEvent.Success(value)
|
when (this) {
|
||||||
is SuspendableEvent.Failure -> transform(error)
|
is SuspendableEvent.Success -> SuspendableEvent.Success(value)
|
||||||
|
is SuspendableEvent.Failure -> transform(error)
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
SuspendableEvent.error(ex as E)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.any(
|
suspend inline fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.any(
|
||||||
@ -89,7 +100,7 @@ suspend fun <V : Any?, E : Throwable> List<SuspendableEvent<V, E>>.lift(): Suspe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class SuspendableEvent<out V : Any?, out E : Throwable> {
|
sealed class SuspendableEvent<out V : Any?, out E : Throwable>: ReadOnlyProperty<Any?,V> {
|
||||||
|
|
||||||
abstract operator fun component1(): V?
|
abstract operator fun component1(): V?
|
||||||
abstract operator fun component2(): E?
|
abstract operator fun component2(): E?
|
||||||
@ -115,6 +126,8 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable> {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is Success<*, *> && value == other.value
|
return other is Success<*, *> && value == other.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
|
||||||
}
|
}
|
||||||
|
|
||||||
class Failure<out V : Any?, out E : Throwable>(val error: E) : SuspendableEvent<V, E>() {
|
class Failure<out V : Any?, out E : Throwable>(val error: E) : SuspendableEvent<V, E>() {
|
||||||
@ -133,6 +146,8 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable> {
|
|||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
return other is Failure<*, *> && error == other.error
|
return other is Failure<*, *> && error == other.error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -143,14 +158,14 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable> {
|
|||||||
return value?.let { Success<V, Nothing>(it) } ?: error(fail())
|
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 {
|
suspend inline fun <V : Any?, E : Throwable> of(crossinline block: suspend () -> V): SuspendableEvent<V, E> = try {
|
||||||
Success(f())
|
Success(block())
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
Failure(ex as E)
|
Failure(ex as E)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline operator fun <V : Any?> invoke(crossinline f: suspend () -> V): SuspendableEvent<V, Throwable> = try {
|
suspend inline operator fun <V : Any?> invoke(crossinline block: suspend () -> V): SuspendableEvent<V, Throwable> = try {
|
||||||
Success(f())
|
Success(block())
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
Failure(ex)
|
Failure(ex)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,6 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
|
|
||||||
private lateinit var downloadManager: DownloadManager
|
private lateinit var downloadManager: DownloadManager
|
||||||
private lateinit var downloadService: ParallelExecutor
|
private lateinit var downloadService: ParallelExecutor
|
||||||
private val ytDownloader get() = fetcher.youtubeProvider.ytDownloader
|
|
||||||
private val fetcher: FetchPlatformQueryResult by inject()
|
private val fetcher: FetchPlatformQueryResult by inject()
|
||||||
private val logger: Kermit by inject()
|
private val logger: Kermit by inject()
|
||||||
private val dir: Dir by inject()
|
private val dir: Dir by inject()
|
||||||
@ -161,15 +160,17 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
trackList.forEach {
|
trackList.forEach {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
downloadService.execute {
|
downloadService.execute {
|
||||||
val url = fetcher.findMp3DownloadLink(it)
|
fetcher.findMp3DownloadLink(it).fold(
|
||||||
if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
|
success = { url ->
|
||||||
enqueueDownload(url, it)
|
enqueueDownload(url, it)
|
||||||
} else {
|
},
|
||||||
sendTrackBroadcast(Status.FAILED.name, it)
|
failure = { _ ->
|
||||||
failed++
|
sendTrackBroadcast(Status.FAILED.name, it)
|
||||||
updateNotification()
|
failed++
|
||||||
allTracksStatus[it.title] = DownloadStatus.Failed
|
updateNotification()
|
||||||
}
|
allTracksStatus[it.title] = DownloadStatus.Failed
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,25 +26,33 @@ import com.shabinder.common.di.providers.YoutubeMusic
|
|||||||
import com.shabinder.common.di.providers.YoutubeProvider
|
import com.shabinder.common.di.providers.YoutubeProvider
|
||||||
import com.shabinder.common.di.providers.get
|
import com.shabinder.common.di.providers.get
|
||||||
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.event.coroutines.flatMap
|
||||||
|
import com.shabinder.common.models.event.coroutines.flatMapError
|
||||||
|
import com.shabinder.common.models.event.coroutines.success
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
|
import com.shabinder.common.requireNotNull
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FetchPlatformQueryResult(
|
class FetchPlatformQueryResult(
|
||||||
private val gaanaProvider: GaanaProvider,
|
private val gaanaProvider: GaanaProvider,
|
||||||
val spotifyProvider: SpotifyProvider,
|
private val spotifyProvider: SpotifyProvider,
|
||||||
val youtubeProvider: YoutubeProvider,
|
private val youtubeProvider: YoutubeProvider,
|
||||||
private val saavnProvider: SaavnProvider,
|
private val saavnProvider: SaavnProvider,
|
||||||
val youtubeMusic: YoutubeMusic,
|
private val youtubeMusic: YoutubeMusic,
|
||||||
val youtubeMp3: YoutubeMp3,
|
private val youtubeMp3: YoutubeMp3,
|
||||||
val audioToMp3: AudioToMp3,
|
private val audioToMp3: AudioToMp3,
|
||||||
val dir: Dir
|
val dir: Dir
|
||||||
) {
|
) {
|
||||||
private val db: DownloadRecordDatabaseQueries?
|
private val db: DownloadRecordDatabaseQueries?
|
||||||
get() = dir.db?.downloadRecordDatabaseQueries
|
get() = dir.db?.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
suspend fun query(link: String): PlatformQueryResult? {
|
suspend fun authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient()
|
||||||
|
|
||||||
|
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult,Throwable> {
|
||||||
val result = when {
|
val result = when {
|
||||||
// SPOTIFY
|
// SPOTIFY
|
||||||
link.contains("spotify", true) ->
|
link.contains("spotify", true) ->
|
||||||
@ -63,13 +71,13 @@ class FetchPlatformQueryResult(
|
|||||||
gaanaProvider.query(link)
|
gaanaProvider.query(link)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
null
|
SuspendableEvent.error(SpotiFlyerException.LinkInvalid(link))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result != null) {
|
result.success {
|
||||||
addToDatabaseAsync(
|
addToDatabaseAsync(
|
||||||
link,
|
link,
|
||||||
result.copy() // Send a copy in order to not to freeze Result itself
|
it.copy() // Send a copy in order to not to freeze Result itself
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -79,35 +87,53 @@ class FetchPlatformQueryResult(
|
|||||||
// 2) If Not found try finding on Youtube Music
|
// 2) If Not found try finding on Youtube Music
|
||||||
suspend fun findMp3DownloadLink(
|
suspend fun findMp3DownloadLink(
|
||||||
track: TrackDetails
|
track: TrackDetails
|
||||||
): String? =
|
): SuspendableEvent<String,Throwable> =
|
||||||
if (track.videoID != null) {
|
if (track.videoID != null) {
|
||||||
// We Already have VideoID
|
// We Already have VideoID
|
||||||
when (track.source) {
|
when (track.source) {
|
||||||
Source.JioSaavn -> {
|
Source.JioSaavn -> {
|
||||||
saavnProvider.getSongFromID(track.videoID!!).media_url?.let { m4aLink ->
|
saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song ->
|
||||||
audioToMp3.convertToMp3(m4aLink)
|
song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findHighestQualityMp3Link(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Source.YouTube -> {
|
Source.YouTube -> {
|
||||||
youtubeMp3.getMp3DownloadLink(track.videoID!!)
|
youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull()).flatMapError {
|
||||||
?: youtubeProvider.ytDownloader?.getVideo(track.videoID!!)?.get()?.url?.let { m4aLink ->
|
youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink ->
|
||||||
audioToMp3.convertToMp3(m4aLink)
|
audioToMp3.convertToMp3(m4aLink)
|
||||||
}
|
} ?: throw SpotiFlyerException.YoutubeLinkNotFound(track.videoID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
null/* Do Nothing, We should never reach here for now*/
|
/*We should never reach here for now*/
|
||||||
|
findHighestQualityMp3Link(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First Try Getting A Link From JioSaavn
|
findHighestQualityMp3Link(track)
|
||||||
saavnProvider.findSongDownloadURL(
|
|
||||||
trackName = track.title,
|
|
||||||
trackArtists = track.artists
|
|
||||||
)
|
|
||||||
// Lets Try Fetching Now From Youtube Music
|
|
||||||
?: youtubeMusic.findSongDownloadURL(track)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun findHighestQualityMp3Link(
|
||||||
|
track: TrackDetails
|
||||||
|
):SuspendableEvent<String,Throwable> {
|
||||||
|
// Try Fetching Track from Jio Saavn
|
||||||
|
return saavnProvider.findMp3SongDownloadURL(
|
||||||
|
trackName = track.title,
|
||||||
|
trackArtists = track.artists
|
||||||
|
).flatMapError { saavnError ->
|
||||||
|
// Lets Try Fetching Now From Youtube Music
|
||||||
|
youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError ->
|
||||||
|
// If Both Failed Bubble the Exception Up with both StackTraces
|
||||||
|
SuspendableEvent.error(
|
||||||
|
SpotiFlyerException.DownloadLinkFetchFailed(
|
||||||
|
trackName = track.title,
|
||||||
|
jioSaavnError = saavnError,
|
||||||
|
ytMusicError = ytMusicError
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
|
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
|
||||||
GlobalScope.launch(dispatcherIO) {
|
GlobalScope.launch(dispatcherIO) {
|
||||||
db?.add(
|
db?.add(
|
||||||
|
@ -43,7 +43,7 @@ class TokenStore(
|
|||||||
logger.d { "System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}" }
|
logger.d { "System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}" }
|
||||||
if (Clock.System.now().epochSeconds > token?.expiry ?: 0 || token == null) {
|
if (Clock.System.now().epochSeconds > token?.expiry ?: 0 || token == null) {
|
||||||
logger.d { "Requesting New Token" }
|
logger.d { "Requesting New Token" }
|
||||||
token = authenticateSpotify()
|
token = authenticateSpotify().component1()
|
||||||
GlobalScope.launch { token?.access_token?.let { save(token) } }
|
GlobalScope.launch { token?.access_token?.let { save(token) } }
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
|
@ -2,14 +2,12 @@ package com.shabinder.common.di.audioToMp3
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.models.AudioQuality
|
import com.shabinder.common.models.AudioQuality
|
||||||
import io.ktor.client.HttpClient
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import io.ktor.client.request.forms.formData
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.forms.submitFormWithBinaryData
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.forms.*
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.client.request.headers
|
import io.ktor.http.*
|
||||||
import io.ktor.client.statement.HttpStatement
|
|
||||||
import io.ktor.http.isSuccess
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
interface AudioToMp3 {
|
interface AudioToMp3 {
|
||||||
@ -32,9 +30,9 @@ interface AudioToMp3 {
|
|||||||
suspend fun convertToMp3(
|
suspend fun convertToMp3(
|
||||||
URL: String,
|
URL: String,
|
||||||
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
|
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
|
||||||
): String? {
|
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
|
val activeHost by getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
|
||||||
val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
|
val jobLink by convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
|
||||||
|
|
||||||
// (jobStatus.contains("d")) == COMPLETION
|
// (jobStatus.contains("d")) == COMPLETION
|
||||||
var jobStatus: String
|
var jobStatus: String
|
||||||
@ -54,10 +52,7 @@ interface AudioToMp3 {
|
|||||||
if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
|
if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
|
||||||
} while (!jobStatus.contains("d", true) && retryCount != 0)
|
} while (!jobStatus.contains("d", true) && retryCount != 0)
|
||||||
|
|
||||||
return if (jobStatus.equals("d", true)) {
|
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
|
||||||
// Return MP3 Download Link
|
|
||||||
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
|
|
||||||
} else null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -68,8 +63,8 @@ interface AudioToMp3 {
|
|||||||
URL: String,
|
URL: String,
|
||||||
host: String? = null,
|
host: String? = null,
|
||||||
audioQuality: AudioQuality = AudioQuality.KBPS160,
|
audioQuality: AudioQuality = AudioQuality.KBPS160,
|
||||||
): String {
|
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
val activeHost = host ?: getHost()
|
val activeHost = host ?: getHost().value
|
||||||
val res = client.submitFormWithBinaryData<String>(
|
val res = client.submitFormWithBinaryData<String>(
|
||||||
url = activeHost,
|
url = activeHost,
|
||||||
formData = formData {
|
formData = formData {
|
||||||
@ -87,7 +82,7 @@ interface AudioToMp3 {
|
|||||||
header("Referer", "https://www.onlineconverter.com/")
|
header("Referer", "https://www.onlineconverter.com/")
|
||||||
}
|
}
|
||||||
}.run {
|
}.run {
|
||||||
logger.d { this }
|
// logger.d { this }
|
||||||
dropLast(3) // last 3 are useless unicode char
|
dropLast(3) // last 3 are useless unicode char
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,18 +92,20 @@ interface AudioToMp3 {
|
|||||||
}
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
|
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
|
||||||
return res
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active Host free to process conversion
|
// Active Host free to process conversion
|
||||||
// ex - https://hostveryfast.onlineconverter.com/file/send
|
// ex - https://hostveryfast.onlineconverter.com/file/send
|
||||||
private suspend fun getHost(): String {
|
private suspend fun getHost(): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
return client.get<String>("https://www.onlineconverter.com/get/host") {
|
client.get<String>("https://www.onlineconverter.com/get/host") {
|
||||||
headers {
|
headers {
|
||||||
header("Host", "www.onlineconverter.com")
|
header("Host", "www.onlineconverter.com")
|
||||||
}
|
}
|
||||||
}.also { logger.i("Active Host") { it } }
|
}//.also { logger.i("Active Host") { it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract full Domain from URL
|
// Extract full Domain from URL
|
||||||
// ex - hostveryfast.onlineconverter.com
|
// ex - hostveryfast.onlineconverter.com
|
||||||
private fun String.getHostDomain(): String {
|
private fun String.getHostDomain(): String {
|
||||||
|
@ -33,7 +33,7 @@ class SaavnProvider(
|
|||||||
).apply {
|
).apply {
|
||||||
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
|
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
|
||||||
"song" -> {
|
"song" -> {
|
||||||
getSong(fullLink).let {
|
getSong(fullLink).value.let {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
trackList = listOf(it).toTrackDetails(folderType, subFolder)
|
trackList = listOf(it).toTrackDetails(folderType, subFolder)
|
||||||
@ -42,7 +42,7 @@ class SaavnProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"album" -> {
|
"album" -> {
|
||||||
getAlbum(fullLink)?.let {
|
getAlbum(fullLink).value.let {
|
||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = removeIllegalChars(it.title)
|
subFolder = removeIllegalChars(it.title)
|
||||||
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
||||||
@ -51,7 +51,7 @@ class SaavnProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"featured" -> { // Playlist
|
"featured" -> { // Playlist
|
||||||
getPlaylist(fullLink)?.let {
|
getPlaylist(fullLink).value.let {
|
||||||
folderType = "Playlists"
|
folderType = "Playlists"
|
||||||
subFolder = removeIllegalChars(it.listname)
|
subFolder = removeIllegalChars(it.listname)
|
||||||
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
||||||
|
@ -48,9 +48,9 @@ class SpotifyProvider(
|
|||||||
) : SpotifyRequests {
|
) : SpotifyRequests {
|
||||||
|
|
||||||
override suspend fun authenticateSpotifyClient(override: Boolean) {
|
override suspend fun authenticateSpotifyClient(override: Boolean) {
|
||||||
val token = if (override) authenticateSpotify() else tokenStore.getToken()
|
val token = if (override) authenticateSpotify().component1() else tokenStore.getToken()
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
logger.d { "Please Check your Network Connection" }
|
logger.d { "Spotify Auth Failed: Please Check your Network Connection" }
|
||||||
} else {
|
} else {
|
||||||
logger.d { "Spotify Provider Created with $token" }
|
logger.d { "Spotify Provider Created with $token" }
|
||||||
HttpClient {
|
HttpClient {
|
||||||
@ -183,8 +183,10 @@ class SpotifyProvider(
|
|||||||
coverUrl = playlistObject.images?.firstOrNull()?.url.toString()
|
coverUrl = playlistObject.images?.firstOrNull()?.url.toString()
|
||||||
}
|
}
|
||||||
"episode" -> { // TODO
|
"episode" -> { // TODO
|
||||||
|
throw SpotiFlyerException.FeatureNotImplementedYet()
|
||||||
}
|
}
|
||||||
"show" -> { // TODO
|
"show" -> { // TODO
|
||||||
|
throw SpotiFlyerException.FeatureNotImplementedYet()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link")
|
throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link")
|
||||||
|
@ -46,28 +46,29 @@ import kotlin.math.absoluteValue
|
|||||||
class YoutubeMusic constructor(
|
class YoutubeMusic constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val youtubeMp3: YoutubeMp3,
|
|
||||||
private val youtubeProvider: YoutubeProvider,
|
private val youtubeProvider: YoutubeProvider,
|
||||||
|
private val youtubeMp3: YoutubeMp3,
|
||||||
private val audioToMp3: AudioToMp3
|
private val audioToMp3: AudioToMp3
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
const val tag = "YT Music"
|
const val tag = "YT Music"
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findSongDownloadURL(
|
// Get Downloadable Link
|
||||||
|
suspend fun findMp3SongDownloadURLYT(
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
): SuspendableEvent<String, Throwable> {
|
): SuspendableEvent<String, Throwable> {
|
||||||
val bestMatchVideoID = getYTIDBestMatch(trackDetails)
|
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
|
||||||
return bestMatchVideoID.flatMap { videoID ->
|
// 1 Try getting Link from Yt1s
|
||||||
// Get Downloadable Link
|
|
||||||
youtubeMp3.getMp3DownloadLink(videoID).flatMapError {
|
youtubeMp3.getMp3DownloadLink(videoID).flatMapError {
|
||||||
SuspendableEvent {
|
// 2 if Yt1s failed , Extract Manually
|
||||||
youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink ->
|
youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink ->
|
||||||
audioToMp3.convertToMp3(m4aLink)
|
audioToMp3.convertToMp3(m4aLink)
|
||||||
} ?: throw SpotiFlyerException.YoutubeLinkNotFound(videoID)
|
} ?: throw SpotiFlyerException.YoutubeLinkNotFound(
|
||||||
}
|
videoID,
|
||||||
|
message = "Caught Following Errors While Finding Downloadable Link for $videoID : \n${it.stackTraceToString()}"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,17 +4,20 @@ import co.touchlab.kermit.Kermit
|
|||||||
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
import com.shabinder.common.di.audioToMp3.AudioToMp3
|
||||||
import com.shabinder.common.di.globalJson
|
import com.shabinder.common.di.globalJson
|
||||||
import com.shabinder.common.models.corsApi
|
import com.shabinder.common.models.corsApi
|
||||||
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
|
import com.shabinder.common.models.event.coroutines.map
|
||||||
|
import com.shabinder.common.models.event.coroutines.success
|
||||||
import com.shabinder.common.models.saavn.SaavnAlbum
|
import com.shabinder.common.models.saavn.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
|
||||||
import com.shabinder.common.models.saavn.SaavnSong
|
import com.shabinder.common.models.saavn.SaavnSong
|
||||||
|
import com.shabinder.common.requireNotNull
|
||||||
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
|
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
|
||||||
import io.github.shabinder.utils.getBoolean
|
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.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.*
|
|
||||||
import io.ktor.client.request.*
|
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
|
||||||
@ -32,63 +35,64 @@ interface JioSaavnRequests {
|
|||||||
val httpClient: HttpClient
|
val httpClient: HttpClient
|
||||||
val logger: Kermit
|
val logger: Kermit
|
||||||
|
|
||||||
suspend fun findSongDownloadURL(
|
suspend fun findMp3SongDownloadURL(
|
||||||
trackName: String,
|
trackName: String,
|
||||||
trackArtists: List<String>,
|
trackArtists: List<String>,
|
||||||
): String? {
|
): SuspendableEvent<String,Throwable> = searchForSong(trackName).map { songs ->
|
||||||
val songs = searchForSong(trackName)
|
|
||||||
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
|
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
|
||||||
val m4aLink: String? = bestMatches.keys.firstOrNull()?.let {
|
|
||||||
getSongFromID(it).media_url
|
val m4aLink: String by getSongFromID(bestMatches.keys.first()).map { song ->
|
||||||
|
song.media_url.requireNotNull()
|
||||||
}
|
}
|
||||||
val mp3Link = m4aLink?.let { audioToMp3.convertToMp3(it) }
|
|
||||||
return mp3Link
|
val mp3Link by audioToMp3.convertToMp3(m4aLink)
|
||||||
|
|
||||||
|
mp3Link
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchForSong(
|
suspend fun searchForSong(
|
||||||
query: String,
|
query: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): List<SaavnSearchResult> {
|
): SuspendableEvent<List<SaavnSearchResult>,Throwable> = SuspendableEvent {
|
||||||
/*if (query.startsWith("http") && query.contains("saavn.com")) {
|
|
||||||
return listOf(getSong(query))
|
|
||||||
}*/
|
|
||||||
|
|
||||||
val searchURL = search_base_url + query
|
val searchURL = search_base_url + query
|
||||||
val results = mutableListOf<SaavnSearchResult>()
|
val results = mutableListOf<SaavnSearchResult>()
|
||||||
try {
|
|
||||||
(globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach {
|
(globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject)
|
||||||
(it as? JsonObject)?.formatData()?.let { jsonObject ->
|
.getJsonObject("songs")
|
||||||
|
.getJsonArray("data").requireNotNull().forEach {
|
||||||
|
(it as JsonObject).formatData().let { jsonObject ->
|
||||||
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (e: ServerResponseException) {}
|
|
||||||
return results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLyrics(ID: String): String? {
|
suspend fun getLyrics(ID: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
return try {
|
(Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
|
||||||
(Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
|
.getString("lyrics").requireNotNull()
|
||||||
.getString("lyrics")
|
|
||||||
}catch (e:Exception) { null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSong(
|
suspend fun getSong(
|
||||||
URL: String,
|
URL: String,
|
||||||
fetchLyrics: Boolean = false
|
fetchLyrics: Boolean = false
|
||||||
): SaavnSong {
|
): SuspendableEvent<SaavnSong,Throwable> = SuspendableEvent {
|
||||||
val id = getSongID(URL)
|
val id = getSongID(URL)
|
||||||
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
|
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
|
||||||
.formatData(fetchLyrics)
|
.formatData(fetchLyrics)
|
||||||
return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
|
|
||||||
|
globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSongFromID(
|
suspend fun getSongFromID(
|
||||||
ID: String,
|
ID: String,
|
||||||
fetchLyrics: Boolean = false
|
fetchLyrics: Boolean = false
|
||||||
): SaavnSong {
|
): SuspendableEvent<SaavnSong,Throwable> = SuspendableEvent {
|
||||||
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
|
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
|
||||||
.formatData(fetchLyrics)
|
.formatData(fetchLyrics)
|
||||||
return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
|
|
||||||
|
globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSongID(
|
private suspend fun getSongID(
|
||||||
@ -105,24 +109,19 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getPlaylist(
|
suspend fun getPlaylist(
|
||||||
URL: String,
|
URL: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): SaavnPlaylist? {
|
): SuspendableEvent<SaavnPlaylist,Throwable> = SuspendableEvent {
|
||||||
return try {
|
globalJson.decodeFromJsonElement(
|
||||||
globalJson.decodeFromJsonElement(
|
SaavnPlaylist.serializer(),
|
||||||
SaavnPlaylist.serializer(),
|
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject)
|
||||||
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject)
|
.formatData(includeLyrics)
|
||||||
.formatData(includeLyrics)
|
)
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPlaylistID(
|
private suspend fun getPlaylistID(
|
||||||
URL: String
|
URL: String
|
||||||
): String {
|
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
val res = httpClient.get<String>(URL)
|
val res = httpClient.get<String>(URL)
|
||||||
return try {
|
try {
|
||||||
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||||
@ -132,24 +131,19 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getAlbum(
|
suspend fun getAlbum(
|
||||||
URL: String,
|
URL: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): SaavnAlbum? {
|
): SuspendableEvent<SaavnAlbum,Throwable> = SuspendableEvent {
|
||||||
return try {
|
globalJson.decodeFromJsonElement(
|
||||||
globalJson.decodeFromJsonElement(
|
SaavnAlbum.serializer(),
|
||||||
SaavnAlbum.serializer(),
|
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject)
|
||||||
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL))) as JsonObject)
|
.formatData(includeLyrics)
|
||||||
.formatData(includeLyrics)
|
)
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getAlbumID(
|
private suspend fun getAlbumID(
|
||||||
URL: String
|
URL: String
|
||||||
): String {
|
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
||||||
val res = httpClient.get<String>(URL)
|
val res = httpClient.get<String>(URL)
|
||||||
return try {
|
try {
|
||||||
res.split("\"album_id\":\"")[1].split('"')[0]
|
res.split("\"album_id\":\"")[1].split('"')[0]
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||||
@ -215,8 +209,10 @@ interface JioSaavnRequests {
|
|||||||
// Fetch Lyrics if Requested
|
// Fetch Lyrics if Requested
|
||||||
// Lyrics is HTML Based
|
// Lyrics is HTML Based
|
||||||
if (includeLyrics) {
|
if (includeLyrics) {
|
||||||
if (getBoolean("has_lyrics") == true) {
|
if (getBoolean("has_lyrics") == true && containsKey("id")) {
|
||||||
put("lyrics", getString("id")?.let { getLyrics(it) })
|
getLyrics(getString("id").requireNotNull()).success {
|
||||||
|
put("lyrics", it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
put("lyrics", "")
|
put("lyrics", "")
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package com.shabinder.common.di.spotify
|
package com.shabinder.common.di.spotify
|
||||||
|
|
||||||
import com.shabinder.common.di.globalJson
|
import com.shabinder.common.di.globalJson
|
||||||
|
import com.shabinder.common.models.SpotiFlyerException
|
||||||
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
import com.shabinder.common.models.spotify.TokenData
|
import com.shabinder.common.models.spotify.TokenData
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
@ -29,15 +31,12 @@ import io.ktor.client.request.forms.*
|
|||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlin.native.concurrent.SharedImmutable
|
import kotlin.native.concurrent.SharedImmutable
|
||||||
|
|
||||||
suspend fun authenticateSpotify(): TokenData? {
|
suspend fun authenticateSpotify(): SuspendableEvent<TokenData,Throwable> = SuspendableEvent {
|
||||||
return try {
|
if (methods.value.isInternetAvailable) {
|
||||||
if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
|
spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
|
||||||
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
|
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
|
||||||
} else null
|
}
|
||||||
} catch (e: Exception) {
|
} else throw SpotiFlyerException.NoInternetException()
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SharedImmutable
|
@SharedImmutable
|
||||||
|
@ -19,6 +19,8 @@ package com.shabinder.common.di.youtubeMp3
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.models.corsApi
|
import com.shabinder.common.models.corsApi
|
||||||
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
import com.shabinder.common.models.event.coroutines.SuspendableEvent
|
||||||
|
import com.shabinder.common.models.event.coroutines.flatMap
|
||||||
|
import com.shabinder.common.models.event.coroutines.map
|
||||||
import com.shabinder.common.requireNotNull
|
import com.shabinder.common.requireNotNull
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@ -39,12 +41,11 @@ interface Yt1sMp3 {
|
|||||||
/*
|
/*
|
||||||
* Downloadable Mp3 Link for YT videoID.
|
* Downloadable Mp3 Link for YT videoID.
|
||||||
* */
|
* */
|
||||||
suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent<String,Throwable> = getKey(videoID).flatMap { key ->
|
||||||
getConvertedMp3Link(
|
getConvertedMp3Link(videoID, key).map {
|
||||||
videoID,
|
it["dlink"].requireNotNull()
|
||||||
getKey(videoID).value
|
.jsonPrimitive.content.replace("\"", "")
|
||||||
).value["dlink"].requireNotNull()
|
}
|
||||||
.jsonPrimitive.content.replace("\"", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -83,7 +83,7 @@ interface SpotiFlyerList {
|
|||||||
val queryResult: PlatformQueryResult? = null,
|
val queryResult: PlatformQueryResult? = null,
|
||||||
val link: String = "",
|
val link: String = "",
|
||||||
val trackList: List<TrackDetails> = emptyList(),
|
val trackList: List<TrackDetails> = emptyList(),
|
||||||
val errorOccurred: Exception? = null,
|
val errorOccurred: Throwable? = null,
|
||||||
val askForDonation: Boolean = false,
|
val askForDonation: Boolean = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
data class ResultFetched(val result: PlatformQueryResult, val trackList: List<TrackDetails>) : Result()
|
data class ResultFetched(val result: PlatformQueryResult, val trackList: List<TrackDetails>) : Result()
|
||||||
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
||||||
data class UpdateTrackItem(val item: TrackDetails) : Result()
|
data class UpdateTrackItem(val item: TrackDetails) : Result()
|
||||||
data class ErrorOccurred(val error: Exception) : Result()
|
data class ErrorOccurred(val error: Throwable) : Result()
|
||||||
data class AskForDonation(val isAllowed: Boolean) : Result()
|
data class AskForDonation(val isAllowed: Boolean) : Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,19 +90,17 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
|
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is Intent.SearchLink -> {
|
is Intent.SearchLink -> {
|
||||||
try {
|
val resp = fetchQuery.query(link)
|
||||||
val result = fetchQuery.query(link)
|
resp.fold(
|
||||||
if (result != null) {
|
success = { result ->
|
||||||
result.trackList = result.trackList.toMutableList()
|
result.trackList = result.trackList.toMutableList()
|
||||||
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
|
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
|
||||||
executeIntent(Intent.RefreshTracksStatuses, getState)
|
executeIntent(Intent.RefreshTracksStatuses, getState)
|
||||||
} else {
|
},
|
||||||
throw Exception("An Error Occurred, Check your Link / Connection")
|
failure = {
|
||||||
|
dispatch(Result.ErrorOccurred(it))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
)
|
||||||
e.printStackTrace()
|
|
||||||
dispatch(Result.ErrorOccurred(e))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is Intent.StartDownloadAll -> {
|
is Intent.StartDownloadAll -> {
|
||||||
|
@ -28,7 +28,7 @@ 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.providers.SpotifyProvider
|
import com.shabinder.common.di.dispatcherIO
|
||||||
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
|
||||||
@ -39,7 +39,6 @@ import com.shabinder.common.root.SpotiFlyerRoot.Analytics
|
|||||||
import com.shabinder.common.root.SpotiFlyerRoot.Child
|
import com.shabinder.common.root.SpotiFlyerRoot.Child
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
|
import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
|
||||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -77,8 +76,8 @@ internal class SpotiFlyerRootImpl(
|
|||||||
) {
|
) {
|
||||||
instanceKeeper.ensureNeverFrozen()
|
instanceKeeper.ensureNeverFrozen()
|
||||||
methods.value = dependencies.actions.freeze()
|
methods.value = dependencies.actions.freeze()
|
||||||
/*Authenticate Spotify Client*/
|
/*Init App Launch & Authenticate Spotify Client*/
|
||||||
authenticateSpotify(dependencies.fetchPlatformQueryResult.spotifyProvider)
|
initAppLaunchAndAuthenticateSpotify(dependencies.fetchPlatformQueryResult::authenticateSpotifyClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val router =
|
private val router =
|
||||||
@ -129,11 +128,11 @@ internal class SpotiFlyerRootImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authenticateSpotify(spotifyProvider: SpotifyProvider) {
|
private fun initAppLaunchAndAuthenticateSpotify(authenticator: suspend () -> Unit) {
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
GlobalScope.launch(dispatcherIO) {
|
||||||
analytics.appLaunchEvent()
|
analytics.appLaunchEvent()
|
||||||
/*Authenticate Spotify Client*/
|
/*Authenticate Spotify Client*/
|
||||||
spotifyProvider.authenticateSpotifyClient()
|
authenticator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,3 +30,11 @@ include(
|
|||||||
":console-app",
|
":console-app",
|
||||||
":maintenance-tasks"
|
":maintenance-tasks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
includeBuild("mosaic") {
|
||||||
|
dependencySubstitution {
|
||||||
|
substitute(module("com.jakewharton.mosaic:mosaic-gradle-plugin")).with(project(":mosaic-gradle-plugin"))
|
||||||
|
substitute(module("com.jakewharton.mosaic:mosaic-runtime")).with(project(":mosaic-runtime"))
|
||||||
|
substitute(module("com.jakewharton.mosaic:compose-compiler")).with(project(":compose:compiler"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user