Error Handling WIP

This commit is contained in:
shabinder 2021-06-21 17:55:35 +05:30
parent 0b7b93ba63
commit 8d5e9cdccc
17 changed files with 235 additions and 177 deletions

View File

@ -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 ?: ""}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("\"", "")
} }
/* /*

View File

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

View File

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

View File

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

View File

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