Error Handling Improv

This commit is contained in:
shabinder 2021-06-22 12:53:18 +05:30
parent 979fcc342b
commit b3abc9c4de
7 changed files with 42 additions and 49 deletions

View File

@ -42,6 +42,8 @@ import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.event.coroutines.failure
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -102,10 +104,7 @@ class ForegroundService : Service(), CoroutineScope {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service") createNotificationChannel(channelId, "Downloader Service")
} }
val intent = Intent( val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" }
this,
ForegroundService::class.java
).apply { action = "kill" }
cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT) cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT)
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
} }
@ -119,17 +118,9 @@ class ForegroundService : Service(), CoroutineScope {
intent?.let { intent?.let {
when (it.action) { when (it.action) {
"kill" -> killService() "kill" -> killService()
"query" -> {
val response = Intent().apply {
action = "query_result"
synchronized(trackStatusFlowMap) {
putExtra("tracks", trackStatusFlowMap)
}
}
sendBroadcast(response)
}
} }
} }
// Wake locks and misc tasks from here : // Wake locks and misc tasks from here :
return if (isServiceStarted) { return if (isServiceStarted) {
// Service Already Started // Service Already Started
@ -160,16 +151,16 @@ class ForegroundService : Service(), CoroutineScope {
trackList.forEach { trackList.forEach {
trackStatusFlowMap[it.title] = DownloadStatus.Queued trackStatusFlowMap[it.title] = DownloadStatus.Queued
launch(Dispatchers.IO) { launch {
downloadService.execute { downloadService.execute {
fetcher.findMp3DownloadLink(it).fold( fetcher.findMp3DownloadLink(it).fold(
success = { url -> success = { url ->
enqueueDownload(url, it) enqueueDownload(url, it)
}, },
failure = { _ -> failure = { error ->
failed++ failed++
updateNotification() updateNotification()
trackStatusFlowMap[it.title] = DownloadStatus.Failed trackStatusFlowMap[it.title] = DownloadStatus.Failed(error)
} }
) )
} }
@ -180,7 +171,6 @@ class ForegroundService : Service(), CoroutineScope {
private suspend fun enqueueDownload(url: String, track: TrackDetails) { private suspend fun enqueueDownload(url: String, track: TrackDetails) {
// Initiating Download // Initiating Download
addToNotification("Downloading ${track.title}") addToNotification("Downloading ${track.title}")
logger.d(tag) { "${track.title} Download Started" }
trackStatusFlowMap[track.title] = DownloadStatus.Downloading() trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
// Enqueueing Download // Enqueueing Download
@ -188,26 +178,18 @@ class ForegroundService : Service(), CoroutineScope {
when (it) { when (it) {
is DownloadResult.Error -> { is DownloadResult.Error -> {
logger.d(tag) { it.message } logger.d(tag) { it.message }
removeFromNotification("Downloading ${track.title}")
failed++ failed++
updateNotification() trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message))
removeFromNotification("Downloading ${track.title}")
} }
is DownloadResult.Progress -> { is DownloadResult.Progress -> {
trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress) trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress)
logger.d(tag) { "${track.title} Progress: ${it.progress} %" }
val intent = Intent().apply {
action = "Progress"
putExtra("progress", it.progress)
putExtra("track", track)
}
sendBroadcast(intent)
} }
is DownloadResult.Success -> { is DownloadResult.Success -> {
coroutineScope { coroutineScope {
try { SuspendableEvent {
// Save File and Embed Metadata // Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} } val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
@ -223,11 +205,11 @@ class ForegroundService : Service(), CoroutineScope {
} }
logger.d(tag) { "${track.title} Download Completed" } logger.d(tag) { "${track.title} Download Completed" }
downloaded++ downloaded++
} catch (e: Exception) { }.failure { error ->
e.printStackTrace() error.printStackTrace()
// Download Failed // Download Failed
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
failed++ failed++
trackStatusFlowMap[track.title] = DownloadStatus.Failed(error)
} }
removeFromNotification("Downloading ${track.title}") removeFromNotification("Downloading ${track.title}")
} }

View File

@ -49,5 +49,5 @@ sealed class DownloadStatus : Parcelable {
@Parcelize object Queued : DownloadStatus() @Parcelize object Queued : DownloadStatus()
@Parcelize object NotDownloaded : DownloadStatus() @Parcelize object NotDownloaded : DownloadStatus()
@Parcelize object Converting : DownloadStatus() @Parcelize object Converting : DownloadStatus()
@Parcelize object Failed : DownloadStatus() @Parcelize data class Failed(val error: Throwable) : DownloadStatus()
} }

View File

@ -4,6 +4,7 @@ sealed class SpotiFlyerException(override val message: String): Exception(messag
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 NoInternetException(override val message: String = "Check Your Internet Connection"): SpotiFlyerException(message)
data class MP3ConversionFailed(override val message: String = "MP3 Converter unreachable, probably BUSY !"): SpotiFlyerException(message)
data class NoMatchFound( data class NoMatchFound(
val trackName: String? = null, val trackName: String? = null,

View File

@ -61,7 +61,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { YoutubeProvider(get(), get(), get()) } single { YoutubeProvider(get(), get(), get()) }
single { YoutubeMp3(get(), get()) } single { YoutubeMp3(get(), get()) }
single { YoutubeMusic(get(), get(), get(), get(), get()) } single { YoutubeMusic(get(), get(), get(), get(), get()) }
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get()) } single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
} }
@ThreadLocal @ThreadLocal

View File

@ -16,6 +16,7 @@
package com.shabinder.common.di package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.audioToMp3.AudioToMp3 import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
@ -45,7 +46,8 @@ class FetchPlatformQueryResult(
private val youtubeMusic: YoutubeMusic, private val youtubeMusic: YoutubeMusic,
private val youtubeMp3: YoutubeMp3, private val youtubeMp3: YoutubeMp3,
private val audioToMp3: AudioToMp3, private val audioToMp3: AudioToMp3,
val dir: Dir val dir: Dir,
val logger: Kermit
) { ) {
private val db: DownloadRecordDatabaseQueries? private val db: DownloadRecordDatabaseQueries?
get() = dir.db?.downloadRecordDatabaseQueries get() = dir.db?.downloadRecordDatabaseQueries
@ -120,7 +122,7 @@ class FetchPlatformQueryResult(
trackName = track.title, trackName = track.title,
trackArtists = track.artists trackArtists = track.artists
).flatMapError { saavnError -> ).flatMapError { saavnError ->
// Lets Try Fetching Now From Youtube Music // Saavn Failed, Lets Try Fetching Now From Youtube Music
youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError -> youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError ->
// If Both Failed Bubble the Exception Up with both StackTraces // If Both Failed Bubble the Exception Up with both StackTraces
SuspendableEvent.error( SuspendableEvent.error(

View File

@ -2,8 +2,10 @@ 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 com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.event.coroutines.SuspendableEvent import com.shabinder.common.models.event.coroutines.SuspendableEvent
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
@ -31,8 +33,9 @@ interface AudioToMp3 {
URL: String, URL: String,
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)), audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): SuspendableEvent<String,Throwable> = SuspendableEvent { ): SuspendableEvent<String,Throwable> = SuspendableEvent {
val activeHost by getHost() // ex - https://hostveryfast.onlineconverter.com/file/send // Active Host ex - https://hostveryfast.onlineconverter.com/file/send
val jobLink by convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd // Convert Job Request ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
var (activeHost,jobLink) = convertRequest(URL, audioQuality).value
// (jobStatus.contains("d")) == COMPLETION // (jobStatus.contains("d")) == COMPLETION
var jobStatus: String var jobStatus: String
@ -44,13 +47,22 @@ interface AudioToMp3 {
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}" "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
) )
} catch (e: Exception) { } catch (e: Exception) {
if(e is ClientRequestException && e.response.status.value == 404) {
// No Need to Retry, Host/Converter is Busy
throw SpotiFlyerException.MP3ConversionFailed()
}
// Try Using New Host/Converter
convertRequest(URL, audioQuality).value.also {
activeHost = it.first
jobLink = it.second
}
e.printStackTrace() e.printStackTrace()
"" ""
} }
retryCount-- retryCount--
logger.i("Job Status") { jobStatus } logger.i("Job Status") { jobStatus }
if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio if (!jobStatus.contains("d")) delay(600) // Add Delay , to give Server Time to process audio
} while (!jobStatus.contains("d", true) && retryCount != 0) } while (!jobStatus.contains("d", true) && retryCount > 0)
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download" "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
} }
@ -61,11 +73,10 @@ interface AudioToMp3 {
* */ * */
private suspend fun convertRequest( private suspend fun convertRequest(
URL: String, URL: String,
host: String? = null,
audioQuality: AudioQuality = AudioQuality.KBPS160, audioQuality: AudioQuality = AudioQuality.KBPS160,
): SuspendableEvent<String,Throwable> = SuspendableEvent { ): SuspendableEvent<Pair<String,String>,Throwable> = SuspendableEvent {
val activeHost = host ?: getHost().value val activeHost by getHost()
val res = client.submitFormWithBinaryData<String>( val convertJob = client.submitFormWithBinaryData<String>(
url = activeHost, url = activeHost,
formData = formData { formData = formData {
append("class", "audio") append("class", "audio")
@ -86,14 +97,14 @@ interface AudioToMp3 {
dropLast(3) // last 3 are useless unicode char dropLast(3) // last 3 are useless unicode char
} }
val job = client.get<HttpStatement>(res) { val job = client.get<HttpStatement>(convertJob) {
headers { headers {
header("Host", "www.onlineconverter.com") header("Host", "www.onlineconverter.com")
} }
}.execute() }.execute()
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() } logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
res Pair(activeHost,convertJob)
} }
// Active Host free to process conversion // Active Host free to process conversion

View File

@ -21,7 +21,6 @@ import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.Store
import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.downloadTracks import com.shabinder.common.di.downloadTracks
@ -42,8 +41,6 @@ internal class SpotiFlyerListStoreProvider(
private val link: String, private val link: String,
private val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> private val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
) { ) {
val logger = getLogger()
fun provide(): SpotiFlyerListStore = fun provide(): SpotiFlyerListStore =
object : object :
SpotiFlyerListStore, SpotiFlyerListStore,
@ -70,7 +67,7 @@ internal class SpotiFlyerListStoreProvider(
dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also { dir.db?.downloadRecordDatabaseQueries?.getLastInsertId()?.executeAsOneOrNull()?.also {
// See if It's Time we can request for support for maintaining this project or not // See if It's Time we can request for support for maintaining this project or not
logger.d(message = "Database List Last ID: $it", tag = "Database Last ID") fetchQuery.logger.d(message = { "Database List Last ID: $it" }, tag = "Database Last ID")
val offset = dir.getDonationOffset val offset = dir.getDonationOffset
dispatch( dispatch(
Result.AskForSupport( Result.AskForSupport(