Merge pull request #99 from Shabinder/New_DM_Android

DM changed , Error handling and various Fixes
This commit is contained in:
Shabinder Singh 2021-04-29 17:36:40 +05:30 committed by GitHub
commit 9f9a160e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 360 additions and 316 deletions

View File

@ -106,31 +106,19 @@ dependencies {
implementation("com.google.accompanist:accompanist-insets:0.7.1")
//DECOMPOSE
// DECOMPOSE
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
//Firebase
// Firebase
implementation(platform("com.google.firebase:firebase-bom:27.0.0"))
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-perf-ktx")
/*
//Lifecycle
Versions.androidLifecycle.let{
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$it")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$it")
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$it")
}
*/
Extras.Android.apply {
implementation(appUpdator)
implementation(razorpay)
implementation(fetch)
}
implementation(MVIKotlin.mvikotlin)
implementation(MVIKotlin.mvikotlinMain)
@ -139,11 +127,11 @@ dependencies {
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
//Test
// Test
testImplementation("junit:junit:4.13.2")
androidTestImplementation(Androidx.junit)
androidTestImplementation(Androidx.expresso)
//Desugaring
// Desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
}

View File

@ -23,17 +23,19 @@ import com.shabinder.spotiflyer.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.component.KoinComponent
import org.koin.core.logger.Level
class App: Application(), KoinComponent {
override fun onCreate() {
super.onCreate()
appContext = this
val loggingEnabled = true
initKoin {
androidLogger()
initKoin(loggingEnabled) {
androidLogger(Level.NONE) // No virtual method elapsedNow
androidContext(this@App)
modules(appModule)
modules(appModule(loggingEnabled))
}
}
}

View File

@ -66,7 +66,7 @@ import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.*
import com.shabinder.spotiflyer.utils.*
import com.tonyodev.fetch2.Status
import com.shabinder.common.models.Status
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.android.ext.android.inject

View File

@ -16,27 +16,6 @@
package com.shabinder.spotiflyer.di
import com.shabinder.common.database.appContext
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchConfiguration
import org.koin.dsl.module
val appModule = module {
single { createFetchInstance() }
}
private fun createFetchInstance():Fetch{
val fetchConfiguration =
FetchConfiguration.Builder(appContext).run {
setNamespace("ForegroundDownloaderService")
setDownloadConcurrentLimit(4)
build()
}
return Fetch.run {
setDefaultInstanceConfiguration(fetchConfiguration)
getDefaultInstance()
}.apply {
removeAll() //Starting fresh
}
}
fun appModule(enableLogging:Boolean = false) = module {}

View File

@ -28,7 +28,7 @@ object Versions {
const val ktLint = "10.0.0"
// DI
const val koin = "3.0.1-beta-1"
const val koin = "3.0.1"
// Logger
const val kermit = "0.1.8"
@ -50,6 +50,13 @@ object Versions {
const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0"
}
object HostOS {
// Host OS Properties
private val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows",true)
val isMac = hostOs.startsWith("Mac",true)
val isLinux = hostOs.startsWith("Linux",true)
}
object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}"

View File

@ -21,6 +21,18 @@ plugins {
}
kotlin {
/*IOS Target Can be only built on Mac*/
if(HostOS.isMac){
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) {
iosArm64("ios")
} else {
iosX64("ios")
}
}
jvm("desktop").compilations.all {
kotlinOptions {
useIR = true
@ -40,7 +52,6 @@ kotlin {
// nodejs()
binaries.executable()
}
ios()
sourceSets {
named("commonTest") {
dependencies {

View File

@ -23,12 +23,15 @@ plugins {
kotlin {
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) {
iosArm64("ios")
} else {
iosX64("ios")
/*IOS Target Can be only built on Mac*/
if(HostOS.isMac){
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) {
iosArm64("ios")
} else {
iosX64("ios")
}
}
jvm("desktop").compilations.all {
@ -50,6 +53,7 @@ kotlin {
// nodejs()
binaries.executable()
}
sourceSets {
named("commonMain") {
dependencies {}
@ -86,6 +90,11 @@ kotlin {
implementation("org.jetbrains:kotlin-react-dom:17.0.1-pre.148-kotlin-1.4.30")
}
}
if(HostOS.isMac){
named("iosMain"){
dependencies { }
}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {

View File

@ -35,6 +35,7 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@ -49,6 +50,7 @@ import com.shabinder.common.di.Picture
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.delay
@Composable
fun SpotiFlyerListContent(
@ -57,10 +59,18 @@ fun SpotiFlyerListContent(
) {
val model by component.models.collectAsState(SpotiFlyerList.State())
LaunchedEffect(model.errorOccurred) {
/*Handle if Any Exception Occurred*/
model.errorOccurred?.let {
showPopUpMessage(it.message ?: "An Error Occurred, Check your Link / Connection")
component.onBackPressed()
}
}
Box(modifier = modifier.fillMaxSize()) {
// TODO Better Null Handling
val result = model.queryResult
if (result == null) {
/* Loading Bar */
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier.padding(8.dp))

View File

@ -30,6 +30,5 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt")
}
}
val iosMain by getting
}
}

View File

@ -0,0 +1,64 @@
package com.shabinder.common.models
import kotlin.jvm.JvmStatic
/**
* Enumeration which contains the different states a download
* could go through.
*
* From Fetch
* */
enum class Status constructor(val value: Int) {
/** Indicates when a download is newly created and not yet queued.*/
NONE(0),
/** Indicates when a newly created download is queued.*/
QUEUED(1),
/** Indicates when a download is currently being downloaded.*/
DOWNLOADING(2),
/** Indicates when a download is paused.*/
PAUSED(3),
/** Indicates when a download is completed.*/
COMPLETED(4),
/** Indicates when a download is cancelled.*/
CANCELLED(5),
/** Indicates when a download has failed.*/
FAILED(6),
/** Indicates when a download has been removed and is no longer managed by Fetch.*/
REMOVED(7),
/** Indicates when a download has been deleted and is no longer managed by Fetch.*/
DELETED(8),
/** Indicates when a download has been Added to Fetch for management.*/
ADDED(9);
companion object {
@JvmStatic
fun valueOf(value: Int): Status {
return when (value) {
0 -> NONE
1 -> QUEUED
2 -> DOWNLOADING
3 -> PAUSED
4 -> COMPLETED
5 -> CANCELLED
6 -> FAILED
7 -> REMOVED
8 -> DELETED
9 -> ADDED
else -> NONE
}
}
}
}

View File

@ -50,10 +50,11 @@ kotlin {
implementation(SqlDelight.jdbcDriver)
}
}
val iosMain by getting {
dependencies {
implementation(SqlDelight.nativeDriver)
if(HostOS.isMac){
val iosMain by getting {
dependencies {
implementation(SqlDelight.nativeDriver)
}
}
}
}

View File

@ -20,7 +20,7 @@ plugins {
id("multiplatform-setup")
id("android-setup")
kotlin("plugin.serialization")
kotlin("native.cocoapods") //version "1.4.32"
kotlin("native.cocoapods")
}
version = "1.0"
@ -48,9 +48,9 @@ kotlin {
dependencies {
implementation(project(":common:data-models"))
implementation(project(":common:database"))
implementation("org.jetbrains.kotlinx:atomicfu:0.15.2")
implementation("org.jetbrains.kotlinx:atomicfu:0.16.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.0")
implementation("com.shabinder.fuzzywuzzy:fuzzywuzzy:1.0")
implementation(Ktor.clientCore)
implementation(Ktor.clientSerialization)
@ -69,7 +69,6 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation(Koin.android)
implementation(Ktor.clientAndroid)
implementation(Extras.Android.fetch)
implementation(Extras.Android.razorpay)
api(Extras.mp3agic)
api(Extras.jaudioTagger)

View File

@ -23,6 +23,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaScannerConnection
import android.os.Environment
import android.widget.Toast
import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File
@ -93,53 +94,66 @@ actual class Dir actual constructor(
) {
withContext(Dispatchers.IO){
val songFile = File(trackDetails.outputFilePath)
/*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */
// if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
try {
/*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */
if(!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile.mkdirs()
}
when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> {
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
}
".m4a" -> {
/*FFmpeg.executeAsync(
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
){ _, returnCode ->
when (returnCode) {
Config.RETURN_CODE_SUCCESS -> {
//FFMPEG task Completed
logger.d{ "Async command execution completed successfully." }
scope.launch {
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
}
}
Config.RETURN_CODE_CANCEL -> {
logger.d{"Async command execution cancelled by user."}
}
else -> {
logger.d { "Async command execution failed with rc=$returnCode" }
}
}
}*/
}
else -> {
try {
if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> {
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
} catch (e: Exception) { e.printStackTrace() }
}
".m4a" -> {
/*FFmpeg.executeAsync(
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
){ _, returnCode ->
when (returnCode) {
Config.RETURN_CODE_SUCCESS -> {
//FFMPEG task Completed
logger.d{ "Async command execution completed successfully." }
scope.launch {
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
}
}
Config.RETURN_CODE_CANCEL -> {
logger.d{"Async command execution cancelled by user."}
}
else -> {
logger.d { "Async command execution failed with rc=$returnCode" }
}
}
}*/
}
else -> {
try {
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
} catch (e: Exception) { e.printStackTrace() }
}
}
}catch (e:Exception){
withContext(Dispatchers.Main){
//Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
}
if(songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" }
}
}
}

View File

@ -38,33 +38,25 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.R
import com.shabinder.common.di.getData
import com.shabinder.common.di.*
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.downloader.YoutubeDownloader
import com.shabinder.downloader.models.formats.Format
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2.NetworkType
import com.tonyodev.fetch2.Priority
import com.tonyodev.fetch2.Request
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock
import com.shabinder.common.models.Status
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import java.io.File
import kotlin.coroutines.CoroutineContext
class ForegroundService : Service(), CoroutineScope {
private val tag: String = "Foreground Service"
private val channelId = "ForegroundDownloaderService"
private val notificationId = 101
@ -72,28 +64,25 @@ class ForegroundService : Service(), CoroutineScope {
private var converted = 0 // Total Files Converted
private var downloaded = 0 // Total Files downloaded
private var failed = 0 // Total Files failed
private val isFinished: Boolean
get() = converted + failed == total
private var isSingleDownload: Boolean = false
private val isFinished get() = converted + failed == total
private var isSingleDownload = false
private lateinit var serviceJob: Job
override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO
private val requestMap = hashMapOf<Request, TrackDetails>()
private val allTracksStatus = hashMapOf<String, DownloadStatus>()
private var messageList = mutableListOf("", "", "", "", "")
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
private var messageList = mutableListOf("", "", "", "", "")
private lateinit var cancelIntent: PendingIntent
private lateinit var downloadManager: DownloadManager
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 fetch: Fetch by inject()
private val dir: Dir by inject()
private val ytDownloader: YoutubeDownloader
get() = fetcher.youtubeProvider.ytDownloader
override fun onBind(intent: Intent): IBinder? = null
@ -101,6 +90,7 @@ class ForegroundService : Service(), CoroutineScope {
override fun onCreate() {
super.onCreate()
serviceJob = SupervisorJob()
downloadService = ParallelExecutor(Dispatchers.IO)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service")
}
@ -110,7 +100,6 @@ class ForegroundService : Service(), CoroutineScope {
).apply { action = "kill" }
cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT)
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
fetch.removeAllListeners().addListener(fetchListener)
}
@SuppressLint("WakelockTimeout")
@ -209,143 +198,68 @@ class ForegroundService : Service(), CoroutineScope {
}
private fun enqueueDownload(url: String, track: TrackDetails) {
val request = Request(url, track.outputFilePath).apply {
priority = Priority.NORMAL
networkType = NetworkType.ALL
}
fetch.enqueue(
request,
{ request1 ->
requestMap[request1] = track
logger.d(tag) { "Enqueuing Download" }
},
{ error ->
logger.d(tag) { "Enqueuing Error:${error.throwable}" }
}
)
}
// Initiating Download
addToNotification("Downloading ${track.title}")
logger.d(tag) { "${track.title} Download Started" }
allTracksStatus[track.title] = DownloadStatus.Downloading()
sendTrackBroadcast(Status.DOWNLOADING.name, track)
/**
* Fetch Listener/ Responsible for Fetch Behaviour
**/
private var fetchListener: FetchListener = object : FetchListener {
override fun onQueued(
download: Download,
waitingOnNetwork: Boolean
) {
requestMap[download.request]?.let { sendTrackBroadcast(Status.QUEUED.name, it) }
}
// Enqueueing Download
launch {
downloadService.execute {
downloadFile(url).collect {
when (it) {
is DownloadResult.Error -> {
launch {
logger.d(tag) { it.message }
logger.d(tag) { "${track.title} Requesting Download thru Android DM" }
downloadUsingDM(url, track.outputFilePath, track)
removeFromNotification("Downloading ${track.title}")
downloaded++
}
updateNotification()
sendTrackBroadcast(Status.FAILED.name,track)
}
override fun onRemoved(download: Download) {
// TODO("Not yet implemented")
}
is DownloadResult.Progress -> {
allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
logger.d(tag) { "${track.title} Progress: ${it.progress} %" }
override fun onResumed(download: Download) {
// TODO("Not yet implemented")
}
val intent = Intent().apply {
action = "Progress"
putExtra("progress", it.progress)
putExtra("track", track)
}
sendBroadcast(intent)
}
override fun onStarted(
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int
) {
launch {
val track = requestMap[download.request]
addToNotification("Downloading ${track?.title}")
logger.d(tag) { "${track?.title} Download Started" }
track?.let {
allTracksStatus[it.title] = DownloadStatus.Downloading()
sendTrackBroadcast(Status.DOWNLOADING.name, track)
}
}
}
override fun onWaitingNetwork(download: Download) {
// TODO("Not yet implemented")
}
override fun onAdded(download: Download) {
// TODO("Not yet implemented")
}
override fun onCancelled(download: Download) {
// TODO("Not yet implemented")
}
override fun onCompleted(download: Download) {
val track = requestMap[download.request]
try {
track?.let {
val job = launch { dir.saveFileWithMetadata(byteArrayOf(), it) }
allTracksStatus[it.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting", it)
addToNotification("Processing ${it.title}")
job.invokeOnCompletion { _ ->
converted++
allTracksStatus[it.title] = DownloadStatus.Downloaded
sendTrackBroadcast(Status.COMPLETED.name, it)
removeFromNotification("Processing ${it.title}")
is DownloadResult.Success -> {
try {
// Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) }
allTracksStatus[track.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting", track)
addToNotification("Processing ${track.title}")
job.invokeOnCompletion {
converted++
allTracksStatus[track.title] = DownloadStatus.Downloaded
sendTrackBroadcast(Status.COMPLETED.name, track)
removeFromNotification("Processing ${track.title}")
}
logger.d(tag) { "${track.title} Download Completed" }
} catch (
e: KotlinNullPointerException
) {
// Try downloading using android DM
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
logger.d(tag) { "${track.title} Requesting Download thru Android DM" }
downloadUsingDM(url, track.outputFilePath, track)
}
downloaded++
removeFromNotification("Downloading ${track.title}")
}
}
}
logger.d(tag) { "${track?.title} Download Completed" }
} catch (
e: KotlinNullPointerException
) {
logger.d(tag) { "${track?.title} Download Failed! Error:Fetch!!!!" }
logger.d(tag) { "${track?.title} Requesting Download thru Android DM" }
downloadUsingDM(download.request.url, download.request.file, track!!)
}
downloaded++
requestMap.remove(download.request)
removeFromNotification("Downloading ${track?.title}")
}
override fun onDeleted(download: Download) {
// TODO("Not yet implemented")
}
override fun onDownloadBlockUpdated(
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int
) {
// TODO("Not yet implemented")
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
launch {
val track = requestMap[download.request]
downloaded++
logger.d(tag) { download.error.throwable.toString() }
logger.d(tag) { "${track?.title} Requesting Download thru Android DM" }
downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request)
removeFromNotification("Downloading ${track.title}")
}
updateNotification()
}
override fun onPaused(download: Download) {
// TODO("Not yet implemented")
}
override fun onProgress(
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
launch {
requestMap[download.request]?.run {
allTracksStatus[title] = DownloadStatus.Downloading(download.progress)
logger.d(tag) { "$title ETA: ${etaInMilliSeconds / 1000} sec" }
val intent = Intent().apply {
action = "Progress"
putExtra("progress", download.progress)
putExtra("track", this@run)
}
sendBroadcast(intent)
}
}
}
}
@ -353,7 +267,7 @@ class ForegroundService : Service(), CoroutineScope {
/**
* If fetch Fails , Android Download Manager To RESCUE!!
**/
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails) {
private fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails) {
launch {
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri).apply {
@ -448,8 +362,7 @@ class ForegroundService : Service(), CoroutineScope {
launch {
logger.d(tag) { "Killing Self" }
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
fetch.cancelAll()
fetch.removeAll()
downloadService.close()
updateNotification()
cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir()))
@ -506,7 +419,7 @@ class ForegroundService : Service(), CoroutineScope {
updateNotification()
}
fun sendTrackBroadcast(action: String, track: TrackDetails) {
private fun sendTrackBroadcast(action: String, track: TrackDetails) {
val intent = Intent().apply {
setAction(action)
putExtra("track", track)
@ -514,10 +427,3 @@ class ForegroundService : Service(), CoroutineScope {
this@ForegroundService.sendBroadcast(intent)
}
}
private fun Fetch.removeAllListeners(): Fetch {
for (listener in this.getListenerSet()) {
this.removeListener(listener)
}
return this
}

View File

@ -24,6 +24,7 @@ import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
import io.ktor.client.HttpClient
import io.ktor.client.features.HttpTimeout
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT
@ -65,6 +66,12 @@ fun createHttpClient(enableNetworkLogs: Boolean = false, serializer: KotlinxSeri
install(JsonFeature) {
this.serializer = serializer
}
// Timeout
install(HttpTimeout) {
requestTimeoutMillis = 15000L
connectTimeoutMillis = 15000L
socketTimeoutMillis = 15000L
}
if (enableNetworkLogs) {
install(Logging) {
logger = Logger.DEFAULT
@ -72,4 +79,5 @@ fun createHttpClient(enableNetworkLogs: Boolean = false, serializer: KotlinxSeri
}
}
}
/*Client Active Throughout App's Lifetime*/
val ktorHttpClient = HttpClient {}

View File

@ -53,22 +53,27 @@ expect class Dir (
suspend fun downloadFile(url: String): Flow<DownloadResult> {
return flow {
val client = createHttpClient()
val response = client.get<HttpStatement>(url).execute()
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
val currentRead = response.content.readAvailable(data, offset, data.size)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))
} while (currentRead > 0)
if (response.status.isSuccess()) {
emit(DownloadResult.Success(data))
} else {
emit(DownloadResult.Error("File not downloaded"))
try {
val client = createHttpClient()
val response = client.get<HttpStatement>(url).execute()
val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0
do {
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb
val currentRead = response.content.readAvailable(data, offset, 250000)
offset += currentRead
val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress))
} while (currentRead > 0)
if (response.status.isSuccess()) {
emit(DownloadResult.Success(data))
} else {
emit(DownloadResult.Error("File not downloaded"))
}
client.close()
} catch (e:Exception) {
emit(DownloadResult.Error(e.message ?: "File not downloaded"))
}
client.close()
}
}

View File

@ -46,10 +46,14 @@ class GaanaProvider(
if (type == "Error" || link == "Error") {
return null
}
return gaanaSearch(
type,
link
)
return try {
gaanaSearch(
type,
link
)
} catch (e: Exception) {
null
}
}
private suspend fun gaanaSearch(

View File

@ -100,10 +100,24 @@ class SpotifyProvider(
return null
}
return spotifySearch(
type,
link
)
return try {
spotifySearch(
type,
link
)
}catch (e: Exception){
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
authenticateSpotifyClient(true)
// Retry Search
try {
spotifySearch(
type,
link
)
} catch (e:Exception){
null
}
}
}
private suspend fun spotifySearch(

View File

@ -25,13 +25,18 @@ import io.ktor.client.HttpClient
class YoutubeMp3(
override val httpClient: HttpClient,
private val logger: Kermit,
override val logger: Kermit,
private val dir: Dir,
) : Yt1sMp3 {
suspend fun getMp3DownloadLink(videoID: String): String? = getLinkFromYt1sMp3(videoID)?.let {
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
"https://kind-grasshopper-73.telebit.io/cors/$it"
suspend fun getMp3DownloadLink(videoID: String): String? = try {
getLinkFromYt1sMp3(videoID)?.let {
logger.i { "Download Link: $it" }
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
"https://kind-grasshopper-73.telebit.io/cors/$it"
// "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue
else it
else it
}
} catch (e: Exception) {
null
}
}

View File

@ -20,7 +20,7 @@ import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
import com.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.HttpClient
import io.ktor.client.request.headers
import io.ktor.client.request.post
@ -47,12 +47,17 @@ class YoutubeMusic constructor(
private val tag = "YT Music"
suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? {
return sortByBestMatch(
getYTTracks(query),
trackName = trackDetails.title,
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
return try {
sortByBestMatch(
getYTTracks(query),
trackName = trackDetails.title,
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
} catch (e:Exception) {
// All Internet/Client Related Errors
null
}
}
private suspend fun getYTTracks(query: String): List<YoutubeTrack> {
val youtubeTracks = mutableListOf<YoutubeTrack>()

View File

@ -28,9 +28,13 @@ import io.ktor.client.request.post
import io.ktor.http.Parameters
suspend fun authenticateSpotify(): TokenData? {
return if (isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
} else null
return try {
if (isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
} else null
}catch (e:Exception) {
null
}
}
private val spotifyAuthClient by lazy {

View File

@ -21,6 +21,7 @@ package com.shabinder.common.di.utils
// implementation("org.jetbrains.kotlinx:atomicfu:0.14.4")
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
import com.shabinder.common.di.dispatcherIO
import io.ktor.utils.io.core.Closeable
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException
@ -36,7 +37,7 @@ import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
class ParallelExecutor(
parentContext: CoroutineContext,
parentContext: CoroutineContext = dispatcherIO,
) : Closeable {
private val concurrentOperationLimit = atomic(4)

View File

@ -16,6 +16,7 @@
package com.shabinder.common.di.youtubeMp3
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.gaana.corsApi
import io.ktor.client.HttpClient
import io.ktor.client.request.forms.FormDataContent
@ -31,7 +32,7 @@ import kotlinx.serialization.json.jsonPrimitive
interface Yt1sMp3 {
val httpClient: HttpClient
val logger: Kermit
/*
* Downloadable Mp3 Link for YT videoID.
* */

View File

@ -63,10 +63,6 @@ private suspend fun isInternetAvailable(): Boolean {
actual val isInternetAvailable: Boolean
get() {
return true
/*var result = false
val job = GlobalScope.launch { result = isInternetAvailable() }
while(job.isActive){}
return result*/
}
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)

View File

@ -73,7 +73,8 @@ interface SpotiFlyerList {
data class State(
val queryResult: PlatformQueryResult? = null,
val link: String = "",
val trackList: List<TrackDetails> = emptyList()
val trackList: List<TrackDetails> = emptyList(),
val errorOccurred: Exception? = null
)
}

View File

@ -58,6 +58,7 @@ internal class SpotiFlyerListStoreProvider(
data class ResultFetched(val result: PlatformQueryResult, val trackList: List<TrackDetails>) : Result()
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
data class UpdateTrackItem(val item: TrackDetails) : Result()
data class ErrorOccurred(val error: Exception) : Result()
}
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
@ -74,10 +75,19 @@ internal class SpotiFlyerListStoreProvider(
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) {
is Intent.SearchLink -> fetchQuery.query(link)?.let { result ->
result.trackList = result.trackList.toMutableList()
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
executeIntent(Intent.RefreshTracksStatuses, getState)
is Intent.SearchLink -> {
try {
val result = fetchQuery.query(link)
if( result != null) {
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")
}
} catch (e:Exception) {
dispatch(Result.ErrorOccurred(e))
}
}
is Intent.StartDownloadAll -> {
@ -107,6 +117,7 @@ internal class SpotiFlyerListStoreProvider(
is Result.ResultFetched -> copy(queryResult = result.result, trackList = result.trackList, link = link)
is Result.UpdateTrackList -> copy(trackList = result.list)
is Result.UpdateTrackItem -> updateTrackItem(result.item)
is Result.ErrorOccurred -> copy(errorOccurred = result.error)
}
private fun State.updateTrackItem(item: TrackDetails): State {