android DM Ktor

This commit is contained in:
shabinder 2021-04-28 19:08:08 +05:30
parent 148d56956b
commit 149c9aceb3
5 changed files with 137 additions and 54 deletions

View File

@ -23,6 +23,7 @@ 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() {
@ -32,7 +33,7 @@ class App: Application(), KoinComponent {
val loggingEnabled = true val loggingEnabled = true
initKoin(loggingEnabled) { initKoin(loggingEnabled) {
androidLogger() androidLogger(Level.NONE)
androidContext(this@App) androidContext(this@App)
modules(appModule(loggingEnabled)) modules(appModule(loggingEnabled))
} }

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,65 @@ 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()
}
logger.e { "${songFile.absolutePath} could not be created" }
} }
} }
} }

View File

@ -38,10 +38,9 @@ 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.YoutubeDownloader
@ -59,6 +58,7 @@ 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
@ -80,14 +80,14 @@ class ForegroundService : Service(), CoroutineScope {
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO get() = serviceJob + Dispatchers.IO
private val requestMap = hashMapOf<Request, TrackDetails>() //private val requestMap = hashMapOf<Request, TrackDetails>()
private val allTracksStatus = hashMapOf<String, DownloadStatus>() private val allTracksStatus = hashMapOf<String, DownloadStatus>()
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 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 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 fetch: Fetch by inject()
@ -101,6 +101,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 +111,7 @@ 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) //fetch.removeAllListeners().addListener(fetchListener)
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
@ -209,7 +210,72 @@ 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
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 priority = Priority.NORMAL
networkType = NetworkType.ALL networkType = NetworkType.ALL
} }
@ -222,13 +288,13 @@ class ForegroundService : Service(), CoroutineScope {
{ error -> { error ->
logger.d(tag) { "Enqueuing Error:${error.throwable}" } logger.d(tag) { "Enqueuing Error:${error.throwable}" }
} }
) )*/
} }
/** /**
* Fetch Listener/ Responsible for Fetch Behaviour * Fetch Listener/ Responsible for Fetch Behaviour
**/ **/
private var fetchListener: FetchListener = object : FetchListener { /*private var fetchListener: FetchListener = object : FetchListener {
override fun onQueued( override fun onQueued(
download: Download, download: Download,
waitingOnNetwork: Boolean waitingOnNetwork: Boolean
@ -348,7 +414,7 @@ class ForegroundService : Service(), CoroutineScope {
} }
} }
} }
} }*/
/** /**
* If fetch Fails , Android Download Manager To RESCUE!! * If fetch Fails , Android Download Manager To RESCUE!!
@ -450,6 +516,7 @@ class ForegroundService : Service(), CoroutineScope {
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
fetch.cancelAll() fetch.cancelAll()
fetch.removeAll() fetch.removeAll()
downloadService.close()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir())) cleanFiles(File(dir.defaultDir()))
// TODO cleanFiles(File(dir.imageCacheDir())) // TODO cleanFiles(File(dir.imageCacheDir()))

View File

@ -58,7 +58,8 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
val data = ByteArray(response.contentLength()!!.toInt()) val data = ByteArray(response.contentLength()!!.toInt())
var offset = 0 var offset = 0
do { 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 offset += currentRead
val progress = (offset * 100f / data.size).roundToInt() val progress = (offset * 100f / data.size).roundToInt()
emit(DownloadResult.Progress(progress)) emit(DownloadResult.Progress(progress))

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)