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") implementation("com.google.accompanist:accompanist-insets:0.7.1")
//DECOMPOSE // DECOMPOSE
implementation(Decompose.decompose) implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose) implementation(Decompose.extensionsCompose)
//Firebase // Firebase
implementation(platform("com.google.firebase:firebase-bom:27.0.0")) implementation(platform("com.google.firebase:firebase-bom:27.0.0"))
implementation("com.google.firebase:firebase-analytics-ktx") implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-crashlytics-ktx") implementation("com.google.firebase:firebase-crashlytics-ktx")
implementation("com.google.firebase:firebase-perf-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 { Extras.Android.apply {
implementation(appUpdator) implementation(appUpdator)
implementation(razorpay) implementation(razorpay)
implementation(fetch)
} }
implementation(MVIKotlin.mvikotlin) implementation(MVIKotlin.mvikotlin)
implementation(MVIKotlin.mvikotlinMain) implementation(MVIKotlin.mvikotlinMain)
@ -139,11 +127,11 @@ dependencies {
implementation(Decompose.decompose) implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose) implementation(Decompose.extensionsCompose)
//Test // Test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation(Androidx.junit) androidTestImplementation(Androidx.junit)
androidTestImplementation(Androidx.expresso) androidTestImplementation(Androidx.expresso)
//Desugaring // Desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") 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.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.logger.Level
class App: Application(), KoinComponent { class App: Application(), KoinComponent {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
appContext = this appContext = this
val loggingEnabled = true
initKoin { initKoin(loggingEnabled) {
androidLogger() androidLogger(Level.NONE) // No virtual method elapsedNow
androidContext(this@App) 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.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.* import com.shabinder.common.uikit.*
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.tonyodev.fetch2.Status import com.shabinder.common.models.Status
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject

View File

@ -16,27 +16,6 @@
package com.shabinder.spotiflyer.di 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 import org.koin.dsl.module
val appModule = module { fun appModule(enableLogging:Boolean = false) = 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
}
}

View File

@ -28,7 +28,7 @@ object Versions {
const val ktLint = "10.0.0" const val ktLint = "10.0.0"
// DI // DI
const val koin = "3.0.1-beta-1" const val koin = "3.0.1"
// Logger // Logger
const val kermit = "0.1.8" const val kermit = "0.1.8"
@ -50,6 +50,13 @@ object Versions {
const val targetSdkVersion = 29 const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0" 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 { object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}" val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}" val test = "io.insert-koin:koin-test:${Versions.koin}"

View File

@ -21,6 +21,18 @@ plugins {
} }
kotlin { 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 { jvm("desktop").compilations.all {
kotlinOptions { kotlinOptions {
useIR = true useIR = true
@ -40,7 +52,6 @@ kotlin {
// nodejs() // nodejs()
binaries.executable() binaries.executable()
} }
ios()
sourceSets { sourceSets {
named("commonTest") { named("commonTest") {
dependencies { dependencies {

View File

@ -23,12 +23,15 @@ plugins {
kotlin { kotlin {
val sdkName: String? = System.getenv("SDK_NAME") /*IOS Target Can be only built on Mac*/
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos") if(HostOS.isMac){
if (isiOSDevice) { val sdkName: String? = System.getenv("SDK_NAME")
iosArm64("ios") val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
} else { if (isiOSDevice) {
iosX64("ios") iosArm64("ios")
} else {
iosX64("ios")
}
} }
jvm("desktop").compilations.all { jvm("desktop").compilations.all {
@ -50,6 +53,7 @@ kotlin {
// nodejs() // nodejs()
binaries.executable() binaries.executable()
} }
sourceSets { sourceSets {
named("commonMain") { named("commonMain") {
dependencies {} dependencies {}
@ -86,6 +90,11 @@ kotlin {
implementation("org.jetbrains:kotlin-react-dom:17.0.1-pre.148-kotlin-1.4.30") 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> { 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.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment 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.list.SpotiFlyerList
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 kotlinx.coroutines.delay
@Composable @Composable
fun SpotiFlyerListContent( fun SpotiFlyerListContent(
@ -57,10 +59,18 @@ fun SpotiFlyerListContent(
) { ) {
val model by component.models.collectAsState(SpotiFlyerList.State()) 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()) { Box(modifier = modifier.fillMaxSize()) {
// TODO Better Null Handling
val result = model.queryResult val result = model.queryResult
if (result == null) { if (result == null) {
/* Loading Bar */
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator() CircularProgressIndicator()
Spacer(modifier.padding(8.dp)) Spacer(modifier.padding(8.dp))

View File

@ -30,6 +30,5 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt") 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) implementation(SqlDelight.jdbcDriver)
} }
} }
if(HostOS.isMac){
val iosMain by getting { val iosMain by getting {
dependencies { dependencies {
implementation(SqlDelight.nativeDriver) implementation(SqlDelight.nativeDriver)
}
} }
} }
} }

View File

@ -20,7 +20,7 @@ plugins {
id("multiplatform-setup") id("multiplatform-setup")
id("android-setup") id("android-setup")
kotlin("plugin.serialization") kotlin("plugin.serialization")
kotlin("native.cocoapods") //version "1.4.32" kotlin("native.cocoapods")
} }
version = "1.0" version = "1.0"
@ -48,9 +48,9 @@ kotlin {
dependencies { dependencies {
implementation(project(":common:data-models")) implementation(project(":common:data-models"))
implementation(project(":common:database")) 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-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("com.shabinder.fuzzywuzzy:fuzzywuzzy:1.0")
implementation(Ktor.clientCore) implementation(Ktor.clientCore)
implementation(Ktor.clientSerialization) implementation(Ktor.clientSerialization)
@ -69,7 +69,6 @@ kotlin {
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
implementation(Koin.android) implementation(Koin.android)
implementation(Ktor.clientAndroid) implementation(Ktor.clientAndroid)
implementation(Extras.Android.fetch)
implementation(Extras.Android.razorpay) implementation(Extras.Android.razorpay)
api(Extras.mp3agic) api(Extras.mp3agic)
api(Extras.jaudioTagger) api(Extras.jaudioTagger)

View File

@ -23,6 +23,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.os.Environment import android.os.Environment
import android.widget.Toast
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
@ -93,53 +94,66 @@ actual class Dir actual constructor(
) { ) {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
val songFile = File(trackDetails.outputFilePath) val songFile = File(trackDetails.outputFilePath)
/* try {
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received /*
* */ * Check , if Fetch was Used, File is saved Already, else write byteArray we Received
// if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray) * */
if(!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile.mkdirs()
}
when (trackDetails.outputFilePath.substringAfterLast('.')) { if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
".mp3" -> {
Mp3File(File(songFile.absolutePath)) when (trackDetails.outputFilePath.substringAfterLast('.')) {
.removeAllTags() ".mp3" -> {
.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 {
Mp3File(File(songFile.absolutePath)) Mp3File(File(songFile.absolutePath))
.removeAllTags() .removeAllTags()
.setId3v1Tags(trackDetails) .setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails) .setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath) 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.app.NotificationCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir import com.shabinder.common.di.*
import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.di.R import com.shabinder.common.models.DownloadResult
import com.shabinder.common.di.getData
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.downloader.YoutubeDownloader
import com.shabinder.downloader.models.formats.Format import com.shabinder.downloader.models.formats.Format
import com.tonyodev.fetch2.Download import com.shabinder.common.models.Status
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.io.File import java.io.File
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class ForegroundService : Service(), CoroutineScope { class ForegroundService : Service(), CoroutineScope {
private val tag: String = "Foreground Service" private val tag: String = "Foreground Service"
private val channelId = "ForegroundDownloaderService" private val channelId = "ForegroundDownloaderService"
private val notificationId = 101 private val notificationId = 101
@ -72,28 +64,25 @@ class ForegroundService : Service(), CoroutineScope {
private var converted = 0 // Total Files Converted private var converted = 0 // Total Files Converted
private var downloaded = 0 // Total Files downloaded private var downloaded = 0 // Total Files downloaded
private var failed = 0 // Total Files failed private var failed = 0 // Total Files failed
private val isFinished: Boolean private val isFinished get() = converted + failed == total
get() = converted + failed == total private var isSingleDownload = false
private var isSingleDownload: Boolean = false
private lateinit var serviceJob: Job private lateinit var serviceJob: Job
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO get() = serviceJob + Dispatchers.IO
private val requestMap = hashMapOf<Request, TrackDetails>()
private val allTracksStatus = hashMapOf<String, DownloadStatus>() private val allTracksStatus = hashMapOf<String, DownloadStatus>()
private var messageList = mutableListOf("", "", "", "", "")
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
private var messageList = mutableListOf("", "", "", "", "")
private lateinit var cancelIntent: PendingIntent 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 fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject() private val logger: Kermit by inject()
private val fetch: Fetch by inject()
private val dir: Dir by inject() private val dir: Dir by inject()
private val ytDownloader: YoutubeDownloader
get() = fetcher.youtubeProvider.ytDownloader
override fun onBind(intent: Intent): IBinder? = null override fun onBind(intent: Intent): IBinder? = null
@ -101,6 +90,7 @@ class ForegroundService : Service(), CoroutineScope {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceJob = SupervisorJob() serviceJob = SupervisorJob()
downloadService = ParallelExecutor(Dispatchers.IO)
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")
} }
@ -110,7 +100,6 @@ class ForegroundService : Service(), CoroutineScope {
).apply { action = "kill" } ).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
fetch.removeAllListeners().addListener(fetchListener)
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
@ -209,143 +198,68 @@ class ForegroundService : Service(), CoroutineScope {
} }
private fun enqueueDownload(url: String, track: TrackDetails) { private fun enqueueDownload(url: String, track: TrackDetails) {
val request = Request(url, track.outputFilePath).apply { // Initiating Download
priority = Priority.NORMAL addToNotification("Downloading ${track.title}")
networkType = NetworkType.ALL logger.d(tag) { "${track.title} Download Started" }
} allTracksStatus[track.title] = DownloadStatus.Downloading()
fetch.enqueue( sendTrackBroadcast(Status.DOWNLOADING.name, track)
request,
{ request1 ->
requestMap[request1] = track
logger.d(tag) { "Enqueuing Download" }
},
{ error ->
logger.d(tag) { "Enqueuing Error:${error.throwable}" }
}
)
}
/** // Enqueueing Download
* Fetch Listener/ Responsible for Fetch Behaviour launch {
**/ downloadService.execute {
private var fetchListener: FetchListener = object : FetchListener { downloadFile(url).collect {
override fun onQueued( when (it) {
download: Download, is DownloadResult.Error -> {
waitingOnNetwork: Boolean launch {
) { logger.d(tag) { it.message }
requestMap[download.request]?.let { sendTrackBroadcast(Status.QUEUED.name, it) } 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) { is DownloadResult.Progress -> {
// TODO("Not yet implemented") allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
} logger.d(tag) { "${track.title} Progress: ${it.progress} %" }
override fun onResumed(download: Download) { val intent = Intent().apply {
// TODO("Not yet implemented") action = "Progress"
} putExtra("progress", it.progress)
putExtra("track", track)
}
sendBroadcast(intent)
}
override fun onStarted( is DownloadResult.Success -> {
download: Download, try {
downloadBlocks: List<DownloadBlock>, // Save File and Embed Metadata
totalBlocks: Int val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) }
) { allTracksStatus[track.title] = DownloadStatus.Converting
launch { sendTrackBroadcast("Converting", track)
val track = requestMap[download.request] addToNotification("Processing ${track.title}")
addToNotification("Downloading ${track?.title}") job.invokeOnCompletion {
logger.d(tag) { "${track?.title} Download Started" } converted++
track?.let { allTracksStatus[track.title] = DownloadStatus.Downloaded
allTracksStatus[it.title] = DownloadStatus.Downloading() sendTrackBroadcast(Status.COMPLETED.name, track)
sendTrackBroadcast(Status.DOWNLOADING.name, track) removeFromNotification("Processing ${track.title}")
} }
} logger.d(tag) { "${track.title} Download Completed" }
} } catch (
e: KotlinNullPointerException
override fun onWaitingNetwork(download: Download) { ) {
// TODO("Not yet implemented") // Try downloading using android DM
} logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
logger.d(tag) { "${track.title} Requesting Download thru Android DM" }
override fun onAdded(download: Download) { downloadUsingDM(url, track.outputFilePath, track)
// TODO("Not yet implemented") }
} downloaded++
removeFromNotification("Downloading ${track.title}")
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}")
} }
} }
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!! * 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 { launch {
val uri = Uri.parse(url) val uri = Uri.parse(url)
val request = DownloadManager.Request(uri).apply { val request = DownloadManager.Request(uri).apply {
@ -448,8 +362,7 @@ class ForegroundService : Service(), CoroutineScope {
launch { launch {
logger.d(tag) { "Killing Self" } logger.d(tag) { "Killing Self" }
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
fetch.cancelAll() downloadService.close()
fetch.removeAll()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir())) cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir())) // TODO cleanFiles(File(dir.imageCacheDir()))
@ -506,7 +419,7 @@ class ForegroundService : Service(), CoroutineScope {
updateNotification() updateNotification()
} }
fun sendTrackBroadcast(action: String, track: TrackDetails) { private fun sendTrackBroadcast(action: String, track: TrackDetails) {
val intent = Intent().apply { val intent = Intent().apply {
setAction(action) setAction(action)
putExtra("track", track) putExtra("track", track)
@ -514,10 +427,3 @@ class ForegroundService : Service(), CoroutineScope {
this@ForegroundService.sendBroadcast(intent) 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.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeMusic
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.HttpTimeout
import io.ktor.client.features.json.JsonFeature import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT import io.ktor.client.features.logging.DEFAULT
@ -65,6 +66,12 @@ fun createHttpClient(enableNetworkLogs: Boolean = false, serializer: KotlinxSeri
install(JsonFeature) { install(JsonFeature) {
this.serializer = serializer this.serializer = serializer
} }
// Timeout
install(HttpTimeout) {
requestTimeoutMillis = 15000L
connectTimeoutMillis = 15000L
socketTimeoutMillis = 15000L
}
if (enableNetworkLogs) { if (enableNetworkLogs) {
install(Logging) { install(Logging) {
logger = Logger.DEFAULT logger = Logger.DEFAULT
@ -72,4 +79,5 @@ fun createHttpClient(enableNetworkLogs: Boolean = false, serializer: KotlinxSeri
} }
} }
} }
/*Client Active Throughout App's Lifetime*/
val ktorHttpClient = HttpClient {} val ktorHttpClient = HttpClient {}

View File

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

View File

@ -100,10 +100,24 @@ class SpotifyProvider(
return null return null
} }
return spotifySearch( return try {
type, spotifySearch(
link 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( private suspend fun spotifySearch(

View File

@ -25,13 +25,18 @@ import io.ktor.client.HttpClient
class YoutubeMp3( class YoutubeMp3(
override val httpClient: HttpClient, override val httpClient: HttpClient,
private val logger: Kermit, override val logger: Kermit,
private val dir: Dir, private val dir: Dir,
) : Yt1sMp3 { ) : Yt1sMp3 {
suspend fun getMp3DownloadLink(videoID: String): String? = getLinkFromYt1sMp3(videoID)?.let { suspend fun getMp3DownloadLink(videoID: String): String? = try {
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/) getLinkFromYt1sMp3(videoID)?.let {
"https://kind-grasshopper-73.telebit.io/cors/$it" 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 // "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.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack 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.HttpClient
import io.ktor.client.request.headers import io.ktor.client.request.headers
import io.ktor.client.request.post import io.ktor.client.request.post
@ -47,12 +47,17 @@ class YoutubeMusic constructor(
private val tag = "YT Music" private val tag = "YT Music"
suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? { suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? {
return sortByBestMatch( return try {
getYTTracks(query), sortByBestMatch(
trackName = trackDetails.title, getYTTracks(query),
trackArtists = trackDetails.artists, trackName = trackDetails.title,
trackDurationSec = trackDetails.durationSec trackArtists = trackDetails.artists,
).keys.firstOrNull() trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
} catch (e:Exception) {
// All Internet/Client Related Errors
null
}
} }
private suspend fun getYTTracks(query: String): List<YoutubeTrack> { private suspend fun getYTTracks(query: String): List<YoutubeTrack> {
val youtubeTracks = mutableListOf<YoutubeTrack>() val youtubeTracks = mutableListOf<YoutubeTrack>()

View File

@ -28,9 +28,13 @@ import io.ktor.client.request.post
import io.ktor.http.Parameters import io.ktor.http.Parameters
suspend fun authenticateSpotify(): TokenData? { suspend fun authenticateSpotify(): TokenData? {
return if (isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") { return try {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") }) if (isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
} else null body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
} else null
}catch (e:Exception) {
null
}
} }
private val spotifyAuthClient by lazy { 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") // implementation("org.jetbrains.kotlinx:atomicfu:0.14.4")
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e // Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
import com.shabinder.common.di.dispatcherIO
import io.ktor.utils.io.core.Closeable import io.ktor.utils.io.core.Closeable
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -36,7 +37,7 @@ import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class ParallelExecutor( class ParallelExecutor(
parentContext: CoroutineContext, parentContext: CoroutineContext = dispatcherIO,
) : Closeable { ) : Closeable {
private val concurrentOperationLimit = atomic(4) private val concurrentOperationLimit = atomic(4)

View File

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

View File

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

View File

@ -73,7 +73,8 @@ interface SpotiFlyerList {
data class State( data class State(
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
) )
} }

View File

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