diff --git a/android/src/main/java/com/shabinder/spotiflyer/App.kt b/android/src/main/java/com/shabinder/spotiflyer/App.kt index 19c5ea91..feb0d934 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/App.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/App.kt @@ -23,6 +23,7 @@ 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() { @@ -32,7 +33,7 @@ class App: Application(), KoinComponent { val loggingEnabled = true initKoin(loggingEnabled) { - androidLogger() + androidLogger(Level.NONE) androidContext(this@App) modules(appModule(loggingEnabled)) } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index 595fab55..a4b4bc6a 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -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,65 @@ 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() + } + logger.e { "${songFile.absolutePath} could not be created" } } } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt index d06d88e7..657099bf 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt @@ -38,10 +38,9 @@ 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 @@ -59,6 +58,7 @@ 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 @@ -80,14 +80,14 @@ class ForegroundService : Service(), CoroutineScope { override val coroutineContext: CoroutineContext get() = serviceJob + Dispatchers.IO - private val requestMap = hashMapOf() + //private val requestMap = hashMapOf() private val allTracksStatus = hashMapOf() 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 downloadService: ParallelExecutor private val fetcher: FetchPlatformQueryResult by inject() private val logger: Kermit by inject() private val fetch: Fetch by inject() @@ -101,6 +101,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 +111,7 @@ 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) + //fetch.removeAllListeners().addListener(fetchListener) } @SuppressLint("WakelockTimeout") @@ -209,7 +210,72 @@ class ForegroundService : Service(), CoroutineScope { } private fun enqueueDownload(url: String, track: TrackDetails) { - val request = Request(url, track.outputFilePath).apply { + // Initiating Download + addToNotification("Downloading ${track.title}") + logger.d(tag) { "${track.title} Download Started" } + allTracksStatus[track.title] = DownloadStatus.Downloading() + sendTrackBroadcast(Status.DOWNLOADING.name, track) + + // 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) + } + + is DownloadResult.Progress -> { + allTracksStatus[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 -> { // Todo clear map + 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}") + } + } + } + } + } + + /* val request = Request(url, track.outputFilePath).apply { priority = Priority.NORMAL networkType = NetworkType.ALL } @@ -222,13 +288,13 @@ class ForegroundService : Service(), CoroutineScope { { error -> logger.d(tag) { "Enqueuing Error:${error.throwable}" } } - ) + )*/ } /** * Fetch Listener/ Responsible for Fetch Behaviour **/ - private var fetchListener: FetchListener = object : FetchListener { + /*private var fetchListener: FetchListener = object : FetchListener { override fun onQueued( download: Download, waitingOnNetwork: Boolean @@ -348,7 +414,7 @@ class ForegroundService : Service(), CoroutineScope { } } } - } + }*/ /** * If fetch Fails , Android Download Manager To RESCUE!! @@ -450,6 +516,7 @@ class ForegroundService : Service(), CoroutineScope { messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") fetch.cancelAll() fetch.removeAll() + downloadService.close() updateNotification() cleanFiles(File(dir.defaultDir())) // TODO cleanFiles(File(dir.imageCacheDir())) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index d567e376..96d27ad0 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -58,7 +58,8 @@ suspend fun downloadFile(url: String): Flow { val data = ByteArray(response.contentLength()!!.toInt()) var offset = 0 do { - val currentRead = response.content.readAvailable(data, offset, data.size) + // 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)) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt index 48687115..4a71fad1 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/ParallelExecutor.kt @@ -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)