mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
Merge pull request #99 from Shabinder/New_DM_Android
DM changed , Error handling and various Fixes
This commit is contained in:
commit
9f9a160e88
@ -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")
|
||||||
}
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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}"
|
||||||
|
@ -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 {
|
||||||
|
@ -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> {
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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" }
|
||||||
// "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue
|
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
|
||||||
else it
|
"https://kind-grasshopper-73.telebit.io/cors/$it"
|
||||||
|
// "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>()
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
* */
|
* */
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user