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