mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
Error Handling Improv
This commit is contained in:
parent
979fcc342b
commit
b3abc9c4de
@ -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}")
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user