diff --git a/.gitmodules b/.gitmodules
index d89eb58f..6b523bd4 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "spotiflyer-ios"]
path = spotiflyer-ios
url = https://github.com/Shabinder/spotiflyer-ios
+[submodule "mosaic"]
+ path = mosaic
+ url = https://github.com/JakeWharton/mosaic
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index dfd33500..ab8e3ac0 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -121,17 +121,23 @@ dependencies {
implementation(MVIKotlin.mvikotlinTimeTravel)
// Extras
- Extras.Android.apply {
+ with(Extras.Android) {
implementation(Acra.notification)
implementation(Acra.http)
implementation(appUpdator)
implementation(matomo)
}
+ with(Versions.androidxLifecycle) {
+ implementation("androidx.lifecycle:lifecycle-service:$this")
+ implementation("androidx.lifecycle:lifecycle-common-java8:$this")
+ }
+
+ implementation(Extras.kermit)
//implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.7.0")
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
- implementation("com.google.accompanist:accompanist-insets:0.11.1")
+ implementation("com.google.accompanist:accompanist-insets:0.12.0")
// Test
testImplementation("junit:junit:4.13.2")
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 5fa70abf..04d4f636 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -72,6 +72,6 @@
-
+
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/App.kt b/android/src/main/java/com/shabinder/spotiflyer/App.kt
index 16d02984..b4de2a9d 100644
--- a/android/src/main/java/com/shabinder/spotiflyer/App.kt
+++ b/android/src/main/java/com/shabinder/spotiflyer/App.kt
@@ -19,6 +19,7 @@ package com.shabinder.spotiflyer
import android.app.Application
import android.content.Context
import com.shabinder.common.di.initKoin
+import com.shabinder.common.translations.Strings
import com.shabinder.spotiflyer.di.appModule
import org.acra.config.httpSender
import org.acra.config.notification
@@ -77,10 +78,10 @@ class App: Application(), KoinComponent {
* Obeying `F-Droid Inclusion Privacy Rules`
* */
notification {
- title = getString(R.string.acra_notification_title)
- text = getString(R.string.acra_notification_text)
- channelName = getString(R.string.acra_notification_channel)
- channelDescription = getString(R.string.acra_notification_channel_desc)
+ title = Strings.acraNotificationTitle()
+ text = Strings.acraNotificationText()
+ channelName = "SpotiFlyer_Crashlytics"
+ channelDescription = "Notification Channel to send Spotiflyer Crashes."
sendOnClick = true
}
// Send Crash Report to self hosted Acrarium (FOSS)
diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
index 20382daa..3acd9f81 100644
--- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
+++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt
@@ -17,15 +17,16 @@
package com.shabinder.spotiflyer
import android.annotation.SuppressLint
-import android.content.BroadcastReceiver
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
+import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.activity.ComponentActivity
@@ -51,18 +52,18 @@ import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsHeight
import com.google.accompanist.insets.statusBarsPadding
import com.shabinder.common.di.*
-import com.shabinder.common.di.worker.ForegroundService
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.Actions
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
-import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Analytics
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.uikit.*
+import com.shabinder.spotiflyer.service.ForegroundService
import com.shabinder.spotiflyer.ui.AnalyticsDialog
import com.shabinder.spotiflyer.ui.NetworkDialog
import com.shabinder.spotiflyer.ui.PermissionDialog
@@ -78,14 +79,20 @@ class MainActivity : ComponentActivity() {
private val fetcher: FetchPlatformQueryResult by inject()
private val dir: Dir by inject()
+ private val preferenceManager: PreferenceManager by inject()
private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks get() = root.callBacks
private val trackStatusFlow = MutableSharedFlow>(1)
private var permissionGranted = mutableStateOf(true)
- private lateinit var updateUIReceiver: BroadcastReceiver
- private lateinit var queryReceiver: BroadcastReceiver
private val internetAvailability by lazy { ConnectionLiveData(applicationContext) }
private val tracker get() = (application as App).tracker
+ private val visibleChild get(): SpotiFlyerRoot.Child = root.routerState.value.activeChild.instance
+
+ // Variable for storing instance of our service class
+ var foregroundService: ForegroundService? = null
+
+ // Boolean to check if our activity is bound to service or not
+ var isServiceBound: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -124,18 +131,18 @@ class MainActivity : ComponentActivity() {
AnalyticsDialog(
askForAnalyticsPermission,
enableAnalytics = {
- dir.toggleAnalytics(true)
- dir.firstLaunchDone()
+ preferenceManager.toggleAnalytics(true)
+ preferenceManager.firstLaunchDone()
},
dismissDialog = {
askForAnalyticsPermission = false
- dir.firstLaunchDone()
+ preferenceManager.firstLaunchDone()
}
)
LaunchedEffect(view) {
permissionGranted.value = checkPermissions()
- if(dir.isFirstLaunch) {
+ if(preferenceManager.isFirstLaunch) {
delay(2500)
// Ask For Analytics Permission on first Dialog
askForAnalyticsPermission = true
@@ -149,63 +156,79 @@ class MainActivity : ComponentActivity() {
}
private fun initialise() {
- val isGithubRelease = checkAppSignature(this).also {
- Log.i("SpotiFlyer Github Rel.:",it.toString())
- }
+ val isGithubRelease = checkAppSignature(this)
/*
* Only Send an `Update Notification` on Github Release Builds
* and Track Downloads for all other releases like F-Droid,
* for `Github Downloads` we will track Downloads using : https://tooomm.github.io/github-release-stats/?username=Shabinder&repository=SpotiFlyer
* */
if(isGithubRelease) { checkIfLatestVersion() }
- if(dir.isAnalyticsEnabled && !isGithubRelease) {
+ if(preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
// Download/App Install Event for F-Droid builds
TrackHelper.track().download().with(tracker)
}
handleIntentFromExternalActivity()
+
+ initForegroundService()
}
+ /*START: Foreground Service Handlers*/
+ private fun initForegroundService() {
+ // Start and then Bind to the Service
+ ContextCompat.startForegroundService(
+ this@MainActivity,
+ Intent(this, ForegroundService::class.java)
+ )
+ bindService()
+ }
+
+ /**
+ * Interface for getting the instance of binder from our service class
+ * So client can get instance of our service class and can directly communicate with it.
+ */
+ private val serviceConnection = object : ServiceConnection {
+ val tag = "Service Connection"
+
+ override fun onServiceConnected(className: ComponentName, iBinder: IBinder) {
+ Log.d(tag, "connected to service.")
+ // We've bound to MyService, cast the IBinder and get MyBinder instance
+ val binder = iBinder as ForegroundService.DownloadServiceBinder
+ foregroundService = binder.service
+ isServiceBound = true
+ lifecycleScope.launch {
+ foregroundService?.trackStatusFlowMap?.statusFlow?.let {
+ trackStatusFlow.emitAll(it.conflate())
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(arg0: ComponentName) {
+ Log.d(tag, "disconnected from service.")
+ isServiceBound = false
+ }
+ }
+
+ /*Used to bind to our service class*/
+ private fun bindService() {
+ Intent(this, ForegroundService::class.java).also { intent ->
+ bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ /*Used to unbind from our service class*/
+ private fun unbindService() {
+ Intent(this, ForegroundService::class.java).also {
+ unbindService(serviceConnection)
+ }
+ }
+ /*END: Foreground Service Handlers*/
+
+
@Composable
private fun isInternetAvailableState(): State {
return internetAvailability.observeAsState()
}
- @Suppress("DEPRECATION")
- private fun setUpOnPrefClickListener() {
- // Initialize Builder
- val chooser = StorageChooser.Builder()
- .withActivity(this)
- .withFragmentManager(fragmentManager)
- .withMemoryBar(true)
- .setTheme(StorageChooser.Theme(applicationContext).apply {
- scheme = applicationContext.resources.getIntArray(R.array.default_dark)
- })
- .setDialogTitle("Set Download Directory")
- .allowCustomPath(true)
- .setType(StorageChooser.DIRECTORY_CHOOSER)
- .build()
-
- // get path that the user has chosen
- chooser.setOnSelectListener { path ->
- Log.d("Setting Base Path", path)
- val f = File(path)
- if (f.canWrite()) {
- // hell yeah :)
- dir.setDownloadDirectory(path)
- showPopUpMessage(
- "Download Directory Set to:\n${dir.defaultDir()} "
- )
- }else{
- showPopUpMessage(
- "NO WRITE ACCESS on \n$path ,\nReverting Back to Previous"
- )
- }
- }
-
- // Show dialog whenever you want by
- chooser.show()
- }
-
private fun showPopUpMessage(string: String, long: Boolean = false) {
android.widget.Toast.makeText(
applicationContext,
@@ -225,9 +248,10 @@ class MainActivity : ComponentActivity() {
dependencies = object : SpotiFlyerRoot.Dependencies{
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
override val database = this@MainActivity.dir.db
- override val fetchPlatformQueryResult = this@MainActivity.fetcher
- override val directories: Dir = this@MainActivity.dir
- override val downloadProgressReport: MutableSharedFlow> = trackStatusFlow
+ override val fetchQuery = this@MainActivity.fetcher
+ override val dir: Dir = this@MainActivity.dir
+ override val preferenceManager = this@MainActivity.preferenceManager
+ override val downloadProgressFlow: MutableSharedFlow> = trackStatusFlow
override val actions = object: Actions {
override val platformActions = object : PlatformActions {
@@ -243,12 +267,9 @@ class MainActivity : ComponentActivity() {
)
}
- override fun sendTracksToService(array: ArrayList) {
- for (list in array.chunked(50)) {
- val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
- serviceIntent.putParcelableArrayListExtra("object", list as ArrayList)
- ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
- }
+ override fun sendTracksToService(array: List) {
+ if (foregroundService == null) initForegroundService()
+ foregroundService?.downloadAllTracks(array)
}
}
@@ -256,12 +277,7 @@ class MainActivity : ComponentActivity() {
override fun setDownloadDirectoryAction() = setUpOnPrefClickListener()
- override fun queryActiveTracks() {
- val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java).apply {
- action = "query"
- }
- ContextCompat.startForegroundService(this@MainActivity, serviceIntent)
- }
+ override fun queryActiveTracks() = this@MainActivity.queryActiveTracks()
override fun giveDonation() {
openPlatform("",platformLink = "https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button")
@@ -303,7 +319,7 @@ class MainActivity : ComponentActivity() {
* */
override val analytics = object: Analytics {
override fun appLaunchEvent() {
- if(dir.isAnalyticsEnabled){
+ if(preferenceManager.isAnalyticsEnabled){
TrackHelper.track()
.event("events","App_Launch")
.name("App Launch").with(tracker)
@@ -311,7 +327,7 @@ class MainActivity : ComponentActivity() {
}
override fun homeScreenVisit() {
- if(dir.isAnalyticsEnabled){
+ if(preferenceManager.isAnalyticsEnabled){
// HomeScreen Visit Event
TrackHelper.track().screen("/main_activity/home_screen")
.title("HomeScreen").with(tracker)
@@ -319,7 +335,7 @@ class MainActivity : ComponentActivity() {
}
override fun listScreenVisit() {
- if(dir.isAnalyticsEnabled){
+ if(preferenceManager.isAnalyticsEnabled){
// ListScreen Visit Event
TrackHelper.track().screen("/main_activity/list_screen")
.title("ListScreen").with(tracker)
@@ -327,7 +343,7 @@ class MainActivity : ComponentActivity() {
}
override fun donationDialogVisit() {
- if (dir.isAnalyticsEnabled) {
+ if (preferenceManager.isAnalyticsEnabled) {
// Donation Dialog Open Event
TrackHelper.track().screen("/main_activity/donation_dialog")
.title("DonationDialog").with(tracker)
@@ -337,6 +353,54 @@ class MainActivity : ComponentActivity() {
}
)
+ private fun queryActiveTracks() {
+ lifecycleScope.launch {
+ foregroundService?.trackStatusFlowMap?.let { tracksStatus ->
+ trackStatusFlow.emit(tracksStatus)
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ queryActiveTracks()
+ }
+
+ @Suppress("DEPRECATION")
+ private fun setUpOnPrefClickListener() {
+ // Initialize Builder
+ val chooser = StorageChooser.Builder()
+ .withActivity(this)
+ .withFragmentManager(fragmentManager)
+ .withMemoryBar(true)
+ .setTheme(StorageChooser.Theme(applicationContext).apply {
+ scheme = applicationContext.resources.getIntArray(R.array.default_dark)
+ })
+ .setDialogTitle("Set Download Directory")
+ .allowCustomPath(true)
+ .setType(StorageChooser.DIRECTORY_CHOOSER)
+ .build()
+
+ // get path that the user has chosen
+ chooser.setOnSelectListener { path ->
+ Log.d("Setting Base Path", path)
+ val f = File(path)
+ if (f.canWrite()) {
+ // hell yeah :)
+ preferenceManager.setDownloadDirectory(path)
+ showPopUpMessage(
+ "Download Directory Set to:\n${dir.defaultDir()} "
+ )
+ }else{
+ showPopUpMessage(
+ "NO WRITE ACCESS on \n$path ,\nReverting Back to Previous"
+ )
+ }
+ }
+
+ // Show dialog whenever you want by
+ chooser.show()
+ }
@SuppressLint("ObsoleteSdkInt")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -357,76 +421,6 @@ class MainActivity : ComponentActivity() {
}
}
- /*
- * Broadcast Handlers
- * */
- private fun initializeBroadcast(){
- val intentFilter = IntentFilter().apply {
- addAction(Status.QUEUED.name)
- addAction(Status.FAILED.name)
- addAction(Status.DOWNLOADING.name)
- addAction(Status.COMPLETED.name)
- addAction("Progress")
- addAction("Converting")
- }
- updateUIReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- //Update Flow with latest details
- if (intent != null) {
- val trackDetails = intent.getParcelableExtra("track")
- trackDetails?.let { track ->
- lifecycleScope.launch {
- val latestMap = trackStatusFlow.replayCache.getOrElse(0
- ) { hashMapOf() }.apply {
- this[track.title] = when (intent.action) {
- Status.QUEUED.name -> DownloadStatus.Queued
- Status.FAILED.name -> DownloadStatus.Failed
- Status.DOWNLOADING.name -> DownloadStatus.Downloading()
- "Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0))
- "Converting" -> DownloadStatus.Converting
- Status.COMPLETED.name -> DownloadStatus.Downloaded
- else -> DownloadStatus.NotDownloaded
- }
- }
- trackStatusFlow.emit(latestMap)
- Log.i("Track Update",track.title + track.downloaded.toString())
- }
- }
- }
- }
- }
- val queryFilter = IntentFilter().apply { addAction("query_result") }
- queryReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- //UI update here
- if (intent != null){
- @Suppress("UNCHECKED_CAST")
- val trackList = intent.getSerializableExtra("tracks") as? HashMap?
- trackList?.let { list ->
- Log.i("Service Response", "${list.size} Tracks Active")
- lifecycleScope.launch {
- trackStatusFlow.emit(list)
- }
- }
- }
- }
- }
- registerReceiver(updateUIReceiver, intentFilter)
- registerReceiver(queryReceiver, queryFilter)
- }
-
- override fun onResume() {
- super.onResume()
- initializeBroadcast()
- }
-
- override fun onPause() {
- super.onPause()
- unregisterReceiver(updateUIReceiver)
- unregisterReceiver(queryReceiver)
- }
-
-
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntentFromExternalActivity(intent)
@@ -451,6 +445,11 @@ class MainActivity : ComponentActivity() {
}
}
+ override fun onDestroy() {
+ super.onDestroy()
+ unbindService()
+ }
+
companion object {
const val disableDozeCode = 1223
}
diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt
new file mode 100644
index 00000000..1d2f7540
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/service/ForegroundService.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) 2021 Shabinder Singh
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.shabinder.spotiflyer.service
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_CANCEL_CURRENT
+import android.content.Context
+import android.content.Intent
+import android.os.Binder
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+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.downloadFile
+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.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.failure
+import com.shabinder.common.translations.Strings
+import com.shabinder.spotiflyer.utils.autoclear.AutoClear
+import com.shabinder.spotiflyer.utils.autoclear.autoClear
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
+import java.io.File
+
+class ForegroundService : LifecycleService() {
+
+ private var downloadService: AutoClear = autoClear { ParallelExecutor(Dispatchers.IO) }
+ val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) }
+ private val fetcher: FetchPlatformQueryResult by inject()
+ private val logger: Kermit by inject()
+ private val dir: Dir by inject()
+
+ private var messageList = MutableList(5) { emptyMessage }
+ private var wakeLock: PowerManager.WakeLock? = null
+ private var isServiceStarted = false
+ private val cancelIntent: PendingIntent by lazy {
+ val intent = Intent(this, ForegroundService::class.java).apply { action = "kill" }
+ PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT)
+ }
+
+ /* Variables Holding Download State */
+ private var total = 0
+ private var converted = 0
+ private var downloaded = 0
+ private var failed = 0
+ private val isFinished get() = converted + failed == total
+ private var isSingleDownload = false
+
+ inner class DownloadServiceBinder : Binder() {
+ val service get() = this@ForegroundService
+ }
+ private val myBinder: IBinder = DownloadServiceBinder()
+
+ override fun onBind(intent: Intent): IBinder {
+ super.onBind(intent)
+ return myBinder
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel(CHANNEL_ID, "Downloader Service")
+ }
+
+ @SuppressLint("WakelockTimeout")
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ // Send a notification that service is started
+ Log.i(TAG, "Foreground Service Started.")
+ startForeground(NOTIFICATION_ID, createNotification())
+
+ intent?.let {
+ when (it.action) {
+ "kill" -> killService()
+ }
+ }
+
+ // Wake locks and misc tasks from here :
+ return if (isServiceStarted) {
+ // Service Already Started
+ START_STICKY
+ } else {
+ isServiceStarted = true
+ Log.i(TAG, "Starting the foreground service task")
+ wakeLock =
+ (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
+ newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
+ acquire()
+ }
+ }
+ START_STICKY
+ }
+ }
+
+ /**
+ * Function To Download All Tracks Available in a List
+ **/
+ fun downloadAllTracks(trackList: List) {
+ trackList.size.also { size ->
+ total += size
+ isSingleDownload = (size == 1)
+ updateNotification()
+ }
+
+ trackList.forEach {
+ trackStatusFlowMap[it.title] = DownloadStatus.Queued
+ lifecycleScope.launch {
+ downloadService.value.execute {
+ fetcher.findMp3DownloadLink(it).fold(
+ success = { url ->
+ enqueueDownload(url, it)
+ },
+ failure = { error ->
+ failed++
+ updateNotification()
+ trackStatusFlowMap[it.title] = DownloadStatus.Failed(error)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun enqueueDownload(url: String, track: TrackDetails) {
+ // Initiating Download
+ addToNotification(Message(track.title, DownloadStatus.Downloading()))
+ trackStatusFlowMap[track.title] = DownloadStatus.Downloading()
+
+ // Enqueueing Download
+ downloadFile(url).collect {
+ when (it) {
+ is DownloadResult.Error -> {
+ logger.d(TAG) { it.message }
+ failed++
+ trackStatusFlowMap[track.title] = DownloadStatus.Failed(it.cause ?: Exception(it.message))
+ removeFromNotification(Message(track.title, DownloadStatus.Downloading()))
+ }
+
+ is DownloadResult.Progress -> {
+ trackStatusFlowMap[track.title] = DownloadStatus.Downloading(it.progress)
+ // updateProgressInNotification(Message(track.title,DownloadStatus.Downloading(it.progress)))
+ }
+
+ is DownloadResult.Success -> {
+ coroutineScope {
+ SuspendableEvent {
+ // Save File and Embed Metadata
+ val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
+
+ // Send Converting Status
+ trackStatusFlowMap[track.title] = DownloadStatus.Converting
+ addToNotification(Message(track.title, DownloadStatus.Converting))
+
+ // All Processing Completed for this Track
+ job.invokeOnCompletion {
+ converted++
+ trackStatusFlowMap[track.title] = DownloadStatus.Downloaded
+ removeFromNotification(Message(track.title, DownloadStatus.Converting))
+ }
+ logger.d(TAG) { "${track.title} Download Completed" }
+ downloaded++
+ }.failure { error ->
+ error.printStackTrace()
+ // Download Failed
+ failed++
+ trackStatusFlowMap[track.title] = DownloadStatus.Failed(error)
+ }
+ removeFromNotification(Message(track.title, DownloadStatus.Downloading()))
+ }
+ }
+ }
+ }
+ }
+
+ private fun releaseWakeLock() {
+ logger.d(TAG) { "Releasing Wake Lock" }
+ try {
+ wakeLock?.let {
+ if (it.isHeld) {
+ it.release()
+ }
+ }
+ } catch (e: Exception) {
+ logger.d(TAG) { "Service stopped without being started: ${e.message}" }
+ }
+ isServiceStarted = false
+ }
+
+ @Suppress("SameParameterValue")
+ private fun createNotificationChannel(channelId: String, channelName: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ channelId,
+ channelName, NotificationManager.IMPORTANCE_DEFAULT
+ )
+ channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ service.createNotificationChannel(channel)
+ }
+ }
+
+ /*
+ * Time To Wrap UP
+ * - `Clean Up` and `Stop this Foreground Service`
+ * */
+ private fun killService() {
+ lifecycleScope.launch {
+ logger.d(TAG) { "Killing Self" }
+ messageList = messageList.getEmpty().apply {
+ set(index = 0, Message(Strings.cleaningAndExiting(),DownloadStatus.NotDownloaded))
+ }
+ downloadService.value.close()
+ downloadService.reset()
+ updateNotification()
+ cleanFiles(File(dir.defaultDir()))
+ // cleanFiles(File(dir.imageCacheDir()))
+ messageList = messageList.getEmpty()
+ releaseWakeLock()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stopForeground(true)
+ stopSelf()
+ } else {
+ stopSelf()
+ }
+ }
+ }
+
+ private fun createNotification(): Notification = NotificationCompat.Builder(this, CHANNEL_ID).run {
+ setSmallIcon(R.drawable.ic_download_arrow)
+ setContentTitle("${Strings.total()}: $total ${Strings.completed()}:$converted ${Strings.failed()}:$failed")
+ setSilent(true)
+ setProgress(total,failed+converted,false)
+ setStyle(
+ NotificationCompat.InboxStyle().run {
+ addLine(messageList[messageList.size - 1].asString())
+ addLine(messageList[messageList.size - 2].asString())
+ addLine(messageList[messageList.size - 3].asString())
+ addLine(messageList[messageList.size - 4].asString())
+ addLine(messageList[messageList.size - 5].asString())
+ }
+ )
+ addAction(R.drawable.ic_round_cancel_24, Strings.exit(), cancelIntent)
+ build()
+ }
+
+ private fun addToNotification(message: Message) {
+ messageList.add(message)
+ updateNotification()
+ }
+
+ private fun removeFromNotification(message: Message) {
+ messageList.removeAll { it.title == message.title }
+ updateNotification()
+ }
+
+ private fun updateProgressInNotification(message: Message) {
+ val index = messageList.indexOfFirst { it.title == message.title }
+ messageList[index] = message
+ updateNotification()
+ }
+
+ private fun updateNotification() {
+ val mNotificationManager: NotificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ mNotificationManager.notify(NOTIFICATION_ID, createNotification())
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isFinished) { killService() }
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ super.onTaskRemoved(rootIntent)
+ if (isFinished) { killService() }
+ }
+
+ companion object {
+ private const val TAG: String = "Foreground Service"
+ private const val CHANNEL_ID = "ForegroundDownloaderService"
+ private const val NOTIFICATION_ID = 101
+ }
+}
diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt
new file mode 100644
index 00000000..77a19894
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/service/Message.kt
@@ -0,0 +1,34 @@
+package com.shabinder.spotiflyer.service
+
+import com.shabinder.common.models.DownloadStatus
+import com.shabinder.common.translations.Strings
+
+typealias Message = Pair
+
+val Message.title: String get() = first
+
+val Message.downloadStatus: DownloadStatus get() = second
+
+val Message.progress: String get() = when (downloadStatus) {
+ is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%"
+ is DownloadStatus.Converting -> "-> 100%"
+ is DownloadStatus.Downloaded -> "-> ${Strings.downloadDone}"
+ is DownloadStatus.Failed -> "-> ${Strings.failed()}"
+ is DownloadStatus.Queued -> "-> ${Strings.queued()}"
+ is DownloadStatus.NotDownloaded -> ""
+}
+
+val emptyMessage = Message("",DownloadStatus.NotDownloaded)
+
+// `Progress` is not being shown because we don't get get consistent Updates from Download Fun ,
+// all Progress data is emitted all together from fun
+fun Message.asString(): String {
+ val statusString = when(downloadStatus){
+ is DownloadStatus.Downloading -> Strings.downloading()
+ is DownloadStatus.Converting -> Strings.processing()
+ else -> ""
+ }
+ return "$statusString $title ${""/*progress*/}".trim()
+}
+
+fun List.getEmpty(): MutableList = MutableList(size) { emptyMessage }
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt b/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt
new file mode 100644
index 00000000..be4bcab7
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/service/TrackStatusFlowMap.kt
@@ -0,0 +1,17 @@
+package com.shabinder.spotiflyer.service
+
+import com.shabinder.common.models.DownloadStatus
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+
+class TrackStatusFlowMap(
+ val statusFlow: MutableSharedFlow>,
+ private val scope: CoroutineScope
+): HashMap() {
+ override fun put(key: String, value: DownloadStatus): DownloadStatus? {
+ val res = super.put(key, value)
+ scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
+ return res
+ }
+}
\ No newline at end of file
diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt
similarity index 61%
rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt
rename to android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt
index 06c26c80..309e68fd 100644
--- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt
+++ b/android/src/main/java/com/shabinder/spotiflyer/service/Utils.kt
@@ -1,22 +1,22 @@
-package com.shabinder.common.di.worker
+package com.shabinder.spotiflyer.service
-import co.touchlab.kermit.Kermit
+import android.util.Log
import java.io.File
/**
* Cleaning All Residual Files except Mp3 Files
**/
-fun cleanFiles(dir: File, logger: Kermit) {
+fun cleanFiles(dir: File) {
try {
- logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " }
+ Log.d("File Cleaning","Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
if (file.isDirectory) {
- cleanFiles(file, logger)
+ cleanFiles(file)
} else if (file.isFile) {
if (file.path.toString().substringAfterLast(".") != "mp3") {
- logger.d("Files Cleaning") { "Cleaning ${file.path}" }
+ Log.d("Files Cleaning","Cleaning ${file.path}")
file.delete()
}
}
@@ -24,3 +24,4 @@ fun cleanFiles(dir: File, logger: Kermit) {
}
} catch (e: Exception) { e.printStackTrace() }
}
+
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt
new file mode 100644
index 00000000..a9da91b0
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClear.kt
@@ -0,0 +1,74 @@
+package com.shabinder.spotiflyer.utils.autoclear
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import com.shabinder.common.requireNotNull
+import com.shabinder.spotiflyer.utils.autoclear.AutoClear.Companion.TRIGGER
+import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleCreateAndDestroyObserver
+import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleResumeAndPauseObserver
+import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleStartAndStopObserver
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+class AutoClear(
+ lifecycle: Lifecycle,
+ private val initializer: (() -> T)?,
+ private val trigger: TRIGGER = TRIGGER.ON_CREATE,
+) : ReadWriteProperty {
+
+ companion object {
+ enum class TRIGGER {
+ ON_CREATE,
+ ON_START,
+ ON_RESUME
+ }
+ }
+
+ private var _value: T?
+ get() = observer.value
+ set(value) { observer.value = value }
+
+ val value: T get() = _value.requireNotNull()
+
+ private val observer: LifecycleAutoInitializer by lazy {
+ when(trigger) {
+ TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer)
+ TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer)
+ TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer)
+ }
+ }
+
+ init {
+ lifecycle.addObserver(observer)
+ }
+
+ override fun getValue(thisRef: LifecycleOwner, property: KProperty<*>): T {
+
+ if (_value != null) {
+ return value
+ }
+
+ // If for Some Reason Initializer is not invoked even after Initialisation, invoke it after checking state
+ if (thisRef.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+ return initializer?.invoke().also { _value = it }
+ ?: throw IllegalStateException("The value has not yet been set or no default initializer provided")
+ } else {
+ throw IllegalStateException("Activity might have been destroyed or not initialized yet")
+ }
+ }
+
+ override fun setValue(thisRef: LifecycleOwner, property: KProperty<*>, value: T?) {
+ this._value = value
+ }
+
+ fun reset() {
+ this._value = null
+ }
+}
+
+fun LifecycleOwner.autoClear(
+ trigger: TRIGGER = TRIGGER.ON_CREATE,
+ initializer: () -> T
+): AutoClear {
+ return AutoClear(this.lifecycle, initializer, trigger)
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt
new file mode 100644
index 00000000..63c385fc
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/AutoClearFragment.kt
@@ -0,0 +1,62 @@
+package com.shabinder.spotiflyer.utils.autoclear
+
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.Observer
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+class AutoClearFragment(
+ fragment: Fragment,
+ private val initializer: (() -> T)?
+) : ReadWriteProperty {
+
+ private var _value: T? = null
+
+ init {
+ fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
+ val viewLifecycleOwnerObserver = Observer { viewLifecycleOwner ->
+
+ viewLifecycleOwner?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ _value = null
+ }
+ })
+ }
+
+ override fun onCreate(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.observeForever(viewLifecycleOwnerObserver)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ fragment.viewLifecycleOwnerLiveData.removeObserver(viewLifecycleOwnerObserver)
+ }
+ }
+ )
+ }
+
+ override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
+ val value = _value
+
+ if (value != null) {
+ return value
+ }
+
+ if (thisRef.viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
+ return initializer?.invoke().also { _value = it }
+ ?: throw IllegalStateException("The value has not yet been set or no default initializer provided")
+ } else {
+ throw IllegalStateException("Fragment might have been destroyed or not initialized yet")
+ }
+ }
+
+ override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) {
+ _value = value
+ }
+}
+
+fun Fragment.autoClear(initializer: () -> T): AutoClearFragment {
+ return AutoClearFragment(this, initializer)
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt
new file mode 100644
index 00000000..f1475372
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/LifecycleAutoInitializer.kt
@@ -0,0 +1,7 @@
+package com.shabinder.spotiflyer.utils.autoclear
+
+import androidx.lifecycle.DefaultLifecycleObserver
+
+interface LifecycleAutoInitializer: DefaultLifecycleObserver {
+ var value: T?
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt
new file mode 100644
index 00000000..9219b22c
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleCreateAndDestroyObserver.kt
@@ -0,0 +1,21 @@
+package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
+
+import androidx.lifecycle.LifecycleOwner
+import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
+
+class LifecycleCreateAndDestroyObserver(
+ private val initializer: (() -> T)?
+) : LifecycleAutoInitializer {
+
+ override var value: T? = null
+
+ override fun onCreate(owner: LifecycleOwner) {
+ super.onCreate(owner)
+ value = initializer?.invoke()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ value = null
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt
new file mode 100644
index 00000000..b68feb7a
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleResumeAndPauseObserver.kt
@@ -0,0 +1,21 @@
+package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
+
+import androidx.lifecycle.LifecycleOwner
+import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
+
+class LifecycleResumeAndPauseObserver(
+ private val initializer: (() -> T)?
+) : LifecycleAutoInitializer {
+
+ override var value: T? = null
+
+ override fun onResume(owner: LifecycleOwner) {
+ super.onResume(owner)
+ value = initializer?.invoke()
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ super.onPause(owner)
+ value = null
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt
new file mode 100644
index 00000000..d7c4d079
--- /dev/null
+++ b/android/src/main/java/com/shabinder/spotiflyer/utils/autoclear/lifecycleobservers/LifecycleStartAndStopObserver.kt
@@ -0,0 +1,21 @@
+package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
+
+import androidx.lifecycle.LifecycleOwner
+import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
+
+class LifecycleStartAndStopObserver(
+ private val initializer: (() -> T)?
+) : LifecycleAutoInitializer {
+
+ override var value: T? = null
+
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+ value = initializer?.invoke()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ value = null
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
deleted file mode 100644
index c0eb149b..00000000
--- a/android/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- SpotiFlyer
- About
- History
- Supported Platforms
- Support Development
- Star / Fork the project on Github.
- GitHub
- Translate
- Help us translate this app in your local language.
- Donate
- If you think I deserve to get paid for my work, you can leave me some money here.
- Share
- Share this app with your friends and family.
- Made with
- in India
- OOPS, SpotiFlyer Crashed
- Please Send Crash Report to App Developers, So this unfortunate event may not happen again.
- SpotiFlyer_Crashlytics
- Notification Channel to send Spotiflyer Crashes.
-
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index c73376d8..c4f0b7fc 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -33,12 +33,17 @@ allprojects {
tasks.withType().configureEach {
kotlinOptions {
jvmTarget = "1.8"
- useIR = true
+ freeCompilerArgs = freeCompilerArgs + "-Xopt-in=kotlin.RequiresOptIn"
}
}
afterEvaluate {
project.extensions.findByType()?.let { kmpExt ->
- kmpExt.sourceSets.removeAll { it.name == "androidAndroidTestRelease" }
+ kmpExt.sourceSets.run {
+ all {
+ languageSettings.useExperimentalAnnotation("kotlinx.serialization.ExperimentalSerializationApi")
+ }
+ removeAll { it.name == "androidAndroidTestRelease" }
+ }
}
}
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 48ae8147..6a9fa555 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -31,11 +31,12 @@ repositories {
dependencies {
implementation("com.android.tools.build:gradle:4.1.1")
- implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}")
implementation(JetBrains.Compose.gradlePlugin)
implementation(JetBrains.Kotlin.gradlePlugin)
implementation(JetBrains.Kotlin.serialization)
implementation(SqlDelight.gradlePlugin)
+ implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}")
+ implementation("de.comahe.i18n4k:i18n4k-gradle-plugin:0.1.1")
}
kotlin {
diff --git a/buildSrc/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/buildSrc/src/main/kotlin/Versions.kt
index 7bca1caa..6e64166a 100644
--- a/buildSrc/buildSrc/src/main/kotlin/Versions.kt
+++ b/buildSrc/buildSrc/src/main/kotlin/Versions.kt
@@ -49,7 +49,7 @@ object Versions {
const val minSdkVersion = 21
const val compileSdkVersion = 29
const val targetSdkVersion = 29
- const val androidLifecycle = "2.3.0"
+ const val androidxLifecycle = "2.3.1"
}
object HostOS {
@@ -60,6 +60,10 @@ object HostOS {
val isLinux = hostOs.startsWith("Linux",true)
}
+object MultiPlatformSettings {
+ const val dep = "com.russhwolf:multiplatform-settings-no-arg:0.7.7"
+}
+
object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}"
@@ -141,6 +145,10 @@ object Ktor {
val clientJs = "io.ktor:ktor-client-js:${Versions.ktor}"
}
+object Internationalization {
+ const val dep = "de.comahe.i18n4k:i18n4k-core:0.1.1"
+}
+
object Extras {
const val youtubeDownloader = "io.github.shabinder:youtube-api-dl:1.2"
const val fuzzyWuzzy = "io.github.shabinder:fuzzywuzzy:1.1"
diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt
index 47cc0d40..10ea5bf5 100644
--- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt
+++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/AndroidImages.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.shabinder.common.database.R
+import com.shabinder.common.translations.Strings
import kotlinx.coroutines.flow.MutableStateFlow
actual fun montserratFont() = FontFamily(
@@ -43,7 +44,7 @@ actual fun pristineFont() = FontFamily(
actual fun DownloadImageTick() {
Image(
painterResource(R.drawable.ic_tick),
- "Download Done"
+ Strings.downloadDone()
)
}
@@ -51,7 +52,7 @@ actual fun DownloadImageTick() {
actual fun DownloadImageError() {
Image(
painterResource(R.drawable.ic_error),
- "Error! Cant Download this track"
+ Strings.downloadError()
)
}
@@ -59,7 +60,7 @@ actual fun DownloadImageError() {
actual fun DownloadImageArrow(modifier: Modifier) {
Image(
painterResource(R.drawable.ic_arrow),
- "Start Download",
+ Strings.downloadStart(),
modifier
)
}
diff --git a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
index 71eeb0a2..ceae4b23 100644
--- a/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
+++ b/common/compose/src/androidMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.shabinder.common.models.methods
+import com.shabinder.common.translations.Strings
@OptIn(ExperimentalAnimationApi::class)
@Composable
@@ -44,7 +45,7 @@ actual fun DonationDialog(
) {
Column(Modifier.padding(16.dp)) {
Text(
- "We Need Your Support!",
+ Strings.supportUs(),
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center,
color = colorAccent,
@@ -69,7 +70,7 @@ actual fun DonationDialog(
style = SpotiFlyerTypography.h6
)
Text(
- text = "Worldwide Donations",
+ text = Strings.worldWideDonations(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -92,7 +93,7 @@ actual fun DonationDialog(
style = SpotiFlyerTypography.h6
)
Text(
- text = "International Donations (Outside India).",
+ text = Strings.worldWideDonations(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -115,7 +116,7 @@ actual fun DonationDialog(
style = SpotiFlyerTypography.h6
)
Text(
- text = "Indian Donations (UPI / PayTM / PhonePe / Cards).",
+ text = "${Strings.indianDonations()} (UPI / PayTM / PhonePe / Cards).",
style = SpotiFlyerTypography.subtitle2
)
}
@@ -126,11 +127,11 @@ actual fun DonationDialog(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth()
) {
- OutlinedButton(onClick = onSnooze) {
- Text("Dismiss.")
+ OutlinedButton(onClick = onDismiss) {
+ Text(Strings.dismiss())
}
- TextButton(onClick = onDismiss, colors = ButtonDefaults.buttonColors()) {
- Text("Remind Later!")
+ TextButton(onClick = onSnooze, colors = ButtonDefaults.buttonColors()) {
+ Text(Strings.remindLater())
}
}
}
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt
index 0679396c..edef2cb4 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/Color.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Color
val colorPrimary = Color(0xFFFC5C7D)
val colorPrimaryDark = Color(0xFFCE1CFF)
val colorAccent = Color(0xFF9AB3FF)
+val colorAccentVariant = Color(0xFF3457D5)
val colorRedError = Color(0xFFFF9494)
val colorSuccessGreen = Color(0xFF59C351)
val darkBackgroundColor = Color(0xFF000000)
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt
index af210499..006d2054 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerListUi.kt
@@ -17,12 +17,29 @@
package com.shabinder.common.uikit
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.*
-import androidx.compose.runtime.*
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ExtendedFloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -37,6 +54,8 @@ import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
+import com.shabinder.common.translations.Strings
+import com.shabinder.common.uikit.dialogs.DonationDialogComponent
@OptIn(ExperimentalMaterialApi::class)
@Composable
@@ -49,10 +68,11 @@ fun SpotiFlyerListContent(
LaunchedEffect(model.errorOccurred) {
/*Handle if Any Exception Occurred*/
model.errorOccurred?.let {
- methods.value.showPopUpMessage(it.message ?: "An Error Occurred, Check your Link / Connection")
+ methods.value.showPopUpMessage(it.message ?: Strings.errorOccurred())
component.onBackPressed()
}
}
+
Box(modifier = modifier.fillMaxSize()) {
val result = model.queryResult
if (result == null) {
@@ -60,7 +80,7 @@ fun SpotiFlyerListContent(
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier.padding(8.dp))
- Text("Loading..", style = appNameStyle, color = colorPrimary)
+ Text("${Strings.loading()}...", style = appNameStyle, color = colorPrimary)
}
} else {
@@ -83,25 +103,19 @@ fun SpotiFlyerListContent(
state = listState,
modifier = Modifier.fillMaxSize(),
)
+
// Donation Dialog Visibility
- var visibilty by remember { mutableStateOf(false) }
- DonationDialog(
- isVisible = visibilty,
- onDismiss = {
- visibilty = false
- },
- onSnooze = {
- visibilty = false
- component.snoozeDonationDialog()
- }
- )
+ val (openDonationDialog,dismissDonationDialog,snoozeDonationDialog) = DonationDialogComponent {
+ component.dismissDonationDialogSetOffset()
+ }
+
DownloadAllButton(
onClick = {
component.onDownloadAllClicked(model.trackList)
// Check If we are allowed to show donation Dialog
if (model.askForDonation) {
// Show Donation Dialog
- visibilty = true
+ openDonationDialog()
}
},
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
@@ -129,7 +143,7 @@ fun TrackCard(
ImageLoad(
track.albumArtURL,
{ loadImage() },
- "Album Art",
+ Strings.albumArt(),
modifier = Modifier
.width(70.dp)
.height(70.dp)
@@ -143,7 +157,7 @@ fun TrackCard(
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
) {
Text("${track.artists.firstOrNull()}...", fontSize = 12.sp, maxLines = 1)
- Text("${track.durationSec / 60} min, ${track.durationSec % 60} sec", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("${track.durationSec / 60} ${Strings.minute()}, ${track.durationSec % 60} ${Strings.second()}", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
when (track.downloaded) {
@@ -189,7 +203,7 @@ fun CoverImage(
ImageLoad(
coverURL,
{ loadImage(coverURL, true) },
- "Cover Image",
+ Strings.coverImage(),
modifier = Modifier
.padding(12.dp)
.width(190.dp)
@@ -212,9 +226,9 @@ fun CoverImage(
@Composable
fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
ExtendedFloatingActionButton(
- text = { Text("Download All") },
+ text = { Text(Strings.downloadAll()) },
onClick = onClick,
- icon = { Icon(DownloadAllImage(), "Download All Button", tint = Color(0xFF000000)) },
+ icon = { Icon(DownloadAllImage(), Strings.downloadAll() + Strings.button(), tint = Color(0xFF000000)) },
backgroundColor = colorAccent,
modifier = modifier
)
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt
index d8279536..76ee29f5 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt
@@ -17,21 +17,54 @@
package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
-import androidx.compose.foundation.*
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.*
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Card
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedButton
+import androidx.compose.material.Switch
+import androidx.compose.material.SwitchDefaults
+import androidx.compose.material.Tab
+import androidx.compose.material.TabPosition
+import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults.textFieldColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info
-import androidx.compose.material.icons.rounded.*
-import androidx.compose.runtime.*
+import androidx.compose.material.icons.rounded.CardGiftcard
+import androidx.compose.material.icons.rounded.Edit
+import androidx.compose.material.icons.rounded.Flag
+import androidx.compose.material.icons.rounded.Insights
+import androidx.compose.material.icons.rounded.Share
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -50,11 +83,17 @@ import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
import com.shabinder.common.models.DownloadRecord
import com.shabinder.common.models.methods
+import com.shabinder.common.translations.Strings
+import com.shabinder.common.uikit.dialogs.DonationDialogComponent
@Composable
fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
val model by component.model.subscribeAsState()
+ val (openDonationDialog,_,_) = DonationDialogComponent {
+ component.dismissDonationDialogOffset()
+ }
+
Column {
SearchPanel(
model.link,
@@ -65,14 +104,17 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
HomeTabBar(
model.selectedCategory,
HomeCategory.values(),
- component::selectCategory
+ component::selectCategory,
)
when (model.selectedCategory) {
HomeCategory.About -> AboutColumn(
analyticsEnabled = model.isAnalyticsEnabled,
- donationDialogOpenEvent = { component.analytics.donationDialogVisit() },
- toggleAnalytics = component::toggleAnalytics
+ toggleAnalytics = component::toggleAnalytics,
+ openDonationDialog = {
+ component.analytics.donationDialogVisit()
+ openDonationDialog()
+ }
)
HomeCategory.History -> HistoryColumn(
model.records.sortedByDescending { it.id },
@@ -98,6 +140,7 @@ fun HomeTabBar(
}
TabRow(
+ backgroundColor = transparent,
selectedTabIndex = selectedIndex,
indicator = indicator,
modifier = modifier,
@@ -109,16 +152,16 @@ fun HomeTabBar(
text = {
Text(
text = when (category) {
- HomeCategory.About -> "About"
- HomeCategory.History -> "History"
+ HomeCategory.About -> Strings.about()
+ HomeCategory.History -> Strings.history()
},
style = MaterialTheme.typography.body2
)
},
icon = {
when (category) {
- HomeCategory.About -> Icon(Icons.Outlined.Info, "Info Tab")
- HomeCategory.History -> Icon(Icons.Outlined.History, "History Tab")
+ HomeCategory.About -> Icon(Icons.Outlined.Info, Strings.infoTab())
+ HomeCategory.History -> Icon(Icons.Outlined.History, Strings.historyTab())
}
}
)
@@ -141,9 +184,9 @@ fun SearchPanel(
value = link,
onValueChange = updateLink,
leadingIcon = {
- Icon(Icons.Rounded.Edit, "Link Text Box", tint = Color.LightGray)
+ Icon(Icons.Rounded.Edit, Strings.linkTextBox(), tint = Color.LightGray)
},
- label = { Text(text = "Paste Link Here...", color = Color.LightGray) },
+ label = { Text(text = Strings.pasteLinkHere(), color = Color.LightGray) },
singleLine = true,
textStyle = TextStyle.Default.merge(TextStyle(fontSize = 18.sp, color = Color.White)),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
@@ -170,7 +213,7 @@ fun SearchPanel(
OutlinedButton(
modifier = Modifier.padding(12.dp).wrapContentWidth(),
onClick = {
- if (link.isBlank()) methods.value.showPopUpMessage("Enter A Link!")
+ if (link.isBlank()) methods.value.showPopUpMessage(Strings.enterALink())
else {
// TODO if(!isOnline(ctx)) showPopUpMessage("Check Your Internet Connection") else
onSearch(link)
@@ -186,7 +229,7 @@ fun SearchPanel(
)
)
) {
- Text(text = "Search", style = SpotiFlyerTypography.h6, modifier = Modifier.padding(4.dp))
+ Text(text = Strings.search(), style = SpotiFlyerTypography.h6, modifier = Modifier.padding(4.dp))
}
}
}
@@ -195,7 +238,7 @@ fun SearchPanel(
fun AboutColumn(
modifier: Modifier = Modifier,
analyticsEnabled:Boolean,
- donationDialogOpenEvent: () -> Unit,
+ openDonationDialog: () -> Unit,
toggleAnalytics: (enabled: Boolean) -> Unit
) {
@@ -209,7 +252,7 @@ fun AboutColumn(
) {
Column(modifier.padding(12.dp)) {
Text(
- text = "Supported Platforms",
+ text = Strings.supportedPlatforms(),
style = SpotiFlyerTypography.body1,
color = colorAccent
)
@@ -217,7 +260,7 @@ fun AboutColumn(
Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) {
Icon(
SpotifyLogo(),
- "Open Spotify",
+ "${Strings.open()} Spotify",
tint = Color.Unspecified,
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
onClick = { methods.value.openPlatform("com.spotify.music", "http://open.spotify.com") }
@@ -226,7 +269,7 @@ fun AboutColumn(
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(
GaanaLogo(),
- "Open Gaana",
+ "${Strings.open()} Gaana",
tint = Color.Unspecified,
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
onClick = { methods.value.openPlatform("com.gaana", "https://www.gaana.com") }
@@ -235,7 +278,7 @@ fun AboutColumn(
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(
SaavnLogo(),
- "Open Jio Saavn",
+ "${Strings.open()} Jio Saavn",
tint = Color.Unspecified,
modifier = Modifier.clickable(
onClick = { methods.value.openPlatform("com.jio.media.jiobeats", "https://www.jiosaavn.com/") }
@@ -244,7 +287,7 @@ fun AboutColumn(
Spacer(modifier = modifier.padding(start = 16.dp))
Icon(
YoutubeLogo(),
- "Open Youtube",
+ "${Strings.open()} Youtube",
tint = Color.Unspecified,
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
onClick = { methods.value.openPlatform("com.google.android.youtube", "https://m.youtube.com") }
@@ -253,7 +296,7 @@ fun AboutColumn(
Spacer(modifier = modifier.padding(start = 12.dp))
Icon(
YoutubeMusicLogo(),
- "Open Youtube Music",
+ "${Strings.open()} Youtube Music",
tint = Color.Unspecified,
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
onClick = { methods.value.openPlatform("com.google.android.apps.youtube.music", "https://music.youtube.com/") }
@@ -269,7 +312,7 @@ fun AboutColumn(
) {
Column(modifier.padding(12.dp)) {
Text(
- text = "Support Development",
+ text = Strings.supportDevelopment(),
style = SpotiFlyerTypography.body1,
color = colorAccent
)
@@ -281,7 +324,7 @@ fun AboutColumn(
)
.padding(vertical = 6.dp)
) {
- Icon(GithubLogo(), "Open Project Repo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
+ Icon(GithubLogo(), Strings.openProjectRepo(), Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
@@ -289,7 +332,7 @@ fun AboutColumn(
style = SpotiFlyerTypography.h6
)
Text(
- text = "Star / Fork the project on Github.",
+ text = Strings.starOrForkProject(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -299,51 +342,34 @@ fun AboutColumn(
.clickable(onClick = { methods.value.openPlatform("", "http://github.com/Shabinder/SpotiFlyer") }),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(Icons.Rounded.Flag, "Help Translate", Modifier.size(32.dp))
+ Icon(Icons.Rounded.Flag, Strings.help() + Strings.translate(), Modifier.size(32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
- text = "Translate",
+ text = Strings.translate(),
style = SpotiFlyerTypography.h6
)
Text(
- text = "Help us translate this app in your local language.",
+ text = Strings.helpTranslateDescription(),
style = SpotiFlyerTypography.subtitle2
)
}
}
- var isDonationDialogVisible by remember { mutableStateOf(false) }
-
- DonationDialog(
- isDonationDialogVisible,
- onDismiss = {
- isDonationDialogVisible = false
- },
- onSnooze = {
- isDonationDialogVisible = false
- }
- )
-
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
- .clickable(
- onClick = {
- isDonationDialogVisible = true
- donationDialogOpenEvent()
- }
- ),
+ .clickable(onClick = openDonationDialog),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(Icons.Rounded.CardGiftcard, "Support Developer", Modifier.size(32.dp))
+ Icon(Icons.Rounded.CardGiftcard, Strings.supportDeveloper(), Modifier.size(32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
- text = "Donate",
+ text = Strings.donate(),
style = SpotiFlyerTypography.h6
)
Text(
- text = "If you think I deserve to get paid for my work, you can support me here.",
+ text = Strings.donateDescription(),
// text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.",
style = SpotiFlyerTypography.subtitle2
)
@@ -358,15 +384,15 @@ fun AboutColumn(
),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(Icons.Rounded.Share, "Share SpotiFlyer App", Modifier.size(32.dp))
+ Icon(Icons.Rounded.Share, Strings.share() + Strings.title() + "App", Modifier.size(32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
- text = "Share",
+ text = Strings.share(),
style = SpotiFlyerTypography.h6
)
Text(
- text = "Share this app with your friends and family.",
+ text = Strings.shareDescription(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -380,17 +406,17 @@ fun AboutColumn(
),
verticalAlignment = Alignment.CenterVertically
) {
- Icon(Icons.Rounded.Insights, "Analytics Status", Modifier.size(32.dp))
+ Icon(Icons.Rounded.Insights, Strings.analytics() + Strings.status(), Modifier.size(32.dp))
Spacer(modifier = Modifier.padding(start = 16.dp))
Column(
Modifier.weight(1f)
) {
Text(
- text = "Analytics",
+ text = Strings.analytics(),
style = SpotiFlyerTypography.h6
)
Text(
- text = "Your Data is Anonymized and never shared with 3rd party service",
+ text = Strings.analyticsDescription(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -421,10 +447,10 @@ fun HistoryColumn(
if (it.isEmpty()) {
Column(Modifier.padding(8.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
- Icons.Outlined.Info, "No History Available Yet", modifier = Modifier.size(80.dp),
+ Icons.Outlined.Info, Strings.noHistoryAvailable(), modifier = Modifier.size(80.dp),
colorOffWhite
)
- Text("No History Available", style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light), textAlign = TextAlign.Center)
+ Text(Strings.noHistoryAvailable(), style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light), textAlign = TextAlign.Center)
}
} else {
Box {
@@ -470,7 +496,7 @@ fun DownloadRecordItem(
ImageLoad(
item.coverUrl,
{ loadImage(item.coverUrl) },
- "Album Art",
+ Strings.albumArt(),
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
)
Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f), verticalArrangement = Arrangement.SpaceEvenly) {
@@ -481,12 +507,12 @@ fun DownloadRecordItem(
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
) {
Text(item.type, fontSize = 13.sp, color = colorOffWhite)
- Text("Tracks: ${item.totalFiles}", fontSize = 13.sp, color = colorOffWhite)
+ Text("${Strings.tracks()}: ${item.totalFiles}", fontSize = 13.sp, color = colorOffWhite)
}
}
Image(
ShareImage(),
- "Research",
+ Strings.reSearch(),
modifier = Modifier.clickable(
onClick = {
// if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
@@ -504,7 +530,7 @@ fun HomeCategoryTabIndicator(
) {
Spacer(
modifier.padding(horizontal = 24.dp)
- .height(4.dp)
+ .height(3.dp)
.background(color, RoundedCornerShape(topStartPercent = 100, topEndPercent = 100))
)
}
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt
index 3c0242a9..ad0112b6 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerRootUi.kt
@@ -56,9 +56,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.extensions.compose.jetbrains.Children
import com.arkivanov.decompose.extensions.compose.jetbrains.animation.child.crossfadeScale
-import com.arkivanov.decompose.extensions.compose.jetbrains.asState
+import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRoot.Child
+import com.shabinder.common.translations.Strings
import com.shabinder.common.uikit.splash.Splash
import com.shabinder.common.uikit.splash.SplashState
import com.shabinder.common.uikit.utils.verticalGradientScrim
@@ -125,7 +126,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.d
).then(modifier)
) {
- val activeComponent = component.routerState.asState()
+ val activeComponent = component.routerState.subscribeAsState()
val callBacks = component.callBacks
AppBar(
backgroundColor = appBarColor,
@@ -163,7 +164,7 @@ fun AppBar(
AnimatedVisibility(isBackButtonVisible) {
Icon(
Icons.Rounded.ArrowBackIosNew,
- contentDescription = "Back Button",
+ contentDescription = Strings.backButton(),
modifier = Modifier.clickable { onBackPressed() },
tint = Color.LightGray
)
@@ -171,12 +172,12 @@ fun AppBar(
}
Image(
SpotiFlyerLogo(),
- "SpotiFlyer Logo",
+ Strings.spotiflyerLogo(),
Modifier.size(32.dp),
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
- text = "SpotiFlyer",
+ text = Strings.title(),
style = appNameStyle
)
}
@@ -185,7 +186,7 @@ fun AppBar(
IconButton(
onClick = { setDownloadDirectory() }
) {
- Icon(Icons.Filled.Settings, "Preferences", tint = Color.Gray)
+ Icon(Icons.Filled.Settings, Strings.preferences(), tint = Color.Gray)
}
},
modifier = modifier,
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt
index dc98addb..7b7f3db7 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/dialogs/Donation.kt
@@ -1 +1,33 @@
package com.shabinder.common.uikit.dialogs
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.shabinder.common.uikit.DonationDialog
+
+typealias DonationDialogCallBacks = Triple
+private typealias openAction = () -> Unit
+private typealias dismissAction = () -> Unit
+private typealias snoozeAction = () -> Unit
+
+@Composable
+fun DonationDialogComponent(onDismissExtra: () -> Unit): DonationDialogCallBacks {
+ var isDonationDialogVisible by remember { mutableStateOf(false) }
+ DonationDialog(
+ isDonationDialogVisible,
+ onSnooze = { isDonationDialogVisible = false },
+ onDismiss = {
+ isDonationDialogVisible = false
+ }
+ )
+
+ val openDonationDialog = { isDonationDialogVisible = true }
+ val snoozeDonationDialog = { isDonationDialogVisible = false }
+ val dismissDonationDialog = {
+ onDismissExtra()
+ isDonationDialogVisible = false
+ }
+ return DonationDialogCallBacks(openDonationDialog,dismissDonationDialog,snoozeDonationDialog)
+}
\ No newline at end of file
diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt
index 502bf4a3..6632031e 100644
--- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt
+++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/splash/Splash.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import com.shabinder.common.translations.Strings
import com.shabinder.common.uikit.HeartIcon
import com.shabinder.common.uikit.SpotiFlyerLogo
import com.shabinder.common.uikit.SpotiFlyerTypography
@@ -55,7 +56,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
delay(SplashWaitTime)
currentOnTimeout()
}
- Image(SpotiFlyerLogo(), "SpotiFlyer Logo")
+ Image(SpotiFlyerLogo(), Strings.spotiflyerLogo())
MadeInIndia(Modifier.align(Alignment.BottomCenter))
}
}
@@ -73,21 +74,21 @@ fun MadeInIndia(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
- text = "Made with ",
+ text = "${Strings.madeWith()} ",
color = colorPrimary,
fontSize = 22.sp
)
Spacer(modifier = Modifier.padding(start = 4.dp))
- Icon(HeartIcon(), "Love", tint = Color.Unspecified)
+ Icon(HeartIcon(), Strings.love(), tint = Color.Unspecified)
Spacer(modifier = Modifier.padding(start = 4.dp))
Text(
- text = " in India",
+ text = " ${Strings.inIndia()}",
color = colorPrimary,
fontSize = 22.sp
)
}
Text(
- "by: Shabinder Singh",
+ Strings.byDeveloperName(),
style = SpotiFlyerTypography.h6,
color = colorAccent,
fontSize = 14.sp
diff --git a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
index 19fe6600..e0e692e2 100644
--- a/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
+++ b/common/compose/src/desktopMain/kotlin/com/shabinder/common/uikit/DonationDialog.kt
@@ -20,9 +20,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.v1.Dialog
import com.shabinder.common.models.methods
+import com.shabinder.common.translations.Strings
@OptIn(ExperimentalAnimationApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class)
@Composable
@@ -42,7 +42,7 @@ actual fun DonationDialog(
) {
Column(Modifier.padding(16.dp)) {
Text(
- "Support Us",
+ Strings.supportUs(),
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center,
color = colorAccent,
@@ -67,7 +67,7 @@ actual fun DonationDialog(
style = SpotiFlyerTypography.h6
)
Text(
- text = "International Donations (Outside India).",
+ text = Strings.worldWideDonations(),
style = SpotiFlyerTypography.subtitle2
)
}
@@ -90,7 +90,7 @@ actual fun DonationDialog(
style = SpotiFlyerTypography.h6
)
Text(
- text = "Indian Donations (UPI / PayTM / PhonePe / Cards).",
+ text = "${Strings.indianDonations()} (UPI / PayTM / PhonePe / Cards).",
style = SpotiFlyerTypography.subtitle2
)
}
diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts
index 65775562..2499b0ad 100644
--- a/common/data-models/build.gradle.kts
+++ b/common/data-models/build.gradle.kts
@@ -1,3 +1,5 @@
+import de.comahe.i18n4k.gradle.plugin.i18n4k
+
/*
* * Copyright (c) 2021 Shabinder Singh
* * This program is free software: you can redistribute it and/or modify
@@ -20,11 +22,18 @@ plugins {
id("multiplatform-setup-test")
id("kotlin-parcelize")
kotlin("plugin.serialization")
+ id("de.comahe.i18n4k")
}
val statelyVersion = "1.1.7"
val statelyIsoVersion = "1.1.7-a1"
+i18n4k {
+ inputDirectory = "../../translations"
+ packageName = "com.shabinder.common.translations"
+ // sourceCodeLocales = listOf("en", "de")
+}
+
kotlin {
sourceSets {
/*
@@ -44,6 +53,8 @@ kotlin {
implementation("co.touchlab:stately-concurrency:$statelyVersion")
implementation("co.touchlab:stately-isolate:$statelyIsoVersion")
implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion")
+ implementation(Extras.youtubeDownloader)
+ api(Internationalization.dep)
}
}
androidMain {
diff --git a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt
index 6fd52fd8..0d6adde2 100644
--- a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt
+++ b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt
@@ -14,7 +14,7 @@ actual interface PlatformActions {
fun addToLibrary(path: String)
- fun sendTracksToService(array: ArrayList)
+ fun sendTracksToService(array: List)
}
actual val StubPlatformActions = object : PlatformActions {
@@ -24,5 +24,5 @@ actual val StubPlatformActions = object : PlatformActions {
override fun addToLibrary(path: String) {}
- override fun sendTracksToService(array: ArrayList) {}
+ override fun sendTracksToService(array: List) {}
}
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt
new file mode 100644
index 00000000..98e0e23f
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/Ext.kt
@@ -0,0 +1,3 @@
+package com.shabinder.common
+
+fun T?.requireNotNull() : T = requireNotNull(this)
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt
index 75a9ee21..40e57a12 100644
--- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/CorsProxy.kt
@@ -16,6 +16,9 @@
package com.shabinder.common.models
+import io.github.shabinder.TargetPlatforms
+import io.github.shabinder.activePlatform
+
sealed class CorsProxy(open val url: String) {
data class SelfHostedCorsProxy(override val url: String = "https://cors.spotiflyer.ml/cors/" /*"https://spotiflyer.azurewebsites.net/"*/) : CorsProxy(url)
data class PublicProxyWithExtension(override val url: String = "https://cors.bridged.cc/") : CorsProxy(url)
@@ -45,3 +48,5 @@ sealed class CorsProxy(open val url: String) {
* Default Self Hosted, However ask user to use extension if possible.
* */
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
+
+val corsApi get() = if (activePlatform is TargetPlatforms.Js) corsProxy.url else ""
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt
index b0156ae1..f592c95d 100644
--- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt
@@ -49,5 +49,5 @@ sealed class DownloadStatus : Parcelable {
@Parcelize object Queued : DownloadStatus()
@Parcelize object NotDownloaded : DownloadStatus()
@Parcelize object Converting : DownloadStatus()
- @Parcelize object Failed : DownloadStatus()
+ @Parcelize data class Failed(val error: Throwable) : DownloadStatus()
}
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt
new file mode 100644
index 00000000..2af34bd1
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/SpotiFlyerException.kt
@@ -0,0 +1,43 @@
+package com.shabinder.common.models
+
+import com.shabinder.common.translations.Strings
+
+sealed class SpotiFlyerException(override val message: String): Exception(message) {
+
+ data class FeatureNotImplementedYet(override val message: String = Strings.featureUnImplemented()): SpotiFlyerException(message)
+ data class NoInternetException(override val message: String = Strings.checkInternetConnection()): SpotiFlyerException(message)
+
+ data class MP3ConversionFailed(
+ val extraInfo:String? = null,
+ override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo"
+ ): SpotiFlyerException(message)
+
+ data class UnknownReason(
+ val exception: Throwable? = null,
+ override val message: String = Strings.unknownError()
+ ): SpotiFlyerException(message)
+
+ data class NoMatchFound(
+ val trackName: String? = null,
+ override val message: String = "$trackName : ${Strings.noMatchFound()}"
+ ): SpotiFlyerException(message)
+
+ data class YoutubeLinkNotFound(
+ val videoID: String? = null,
+ override val message: String = "${Strings.noLinkFound()}: $videoID"
+ ): SpotiFlyerException(message)
+
+ data class DownloadLinkFetchFailed(
+ val trackName: String,
+ val jioSaavnError: Throwable,
+ val ytMusicError: Throwable,
+ override val message: String = "${Strings.noLinkFound()}: $trackName," +
+ " \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n " +
+ " \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n "
+ ): SpotiFlyerException(message)
+
+ data class LinkInvalid(
+ val link: String? = null,
+ override val message: String = "${Strings.linkNotValid()}\n ${link ?: ""}"
+ ): SpotiFlyerException(message)
+}
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt
new file mode 100644
index 00000000..0997ab96
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Event.kt
@@ -0,0 +1,207 @@
+package com.shabinder.common.models.event
+
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+inline fun Event<*, *>.getAs() = when (this) {
+ is Event.Success -> value as? X
+ is Event.Failure -> error as? X
+}
+
+inline fun Event.success(f: (V) -> Unit) = fold(f, {})
+
+inline fun Event<*, E>.failure(f: (E) -> Unit) = fold({}, f)
+
+infix fun Event.or(fallback: V) = when (this) {
+ is Event.Success -> this
+ else -> Event.Success(fallback)
+}
+
+inline infix fun Event.getOrElse(fallback: (E) -> V): V {
+ return when (this) {
+ is Event.Success -> value
+ is Event.Failure -> fallback(error)
+ }
+}
+
+fun Event.getOrNull(): V? {
+ return when (this) {
+ is Event.Success -> value
+ is Event.Failure -> null
+ }
+}
+
+fun Event.getThrowableOrNull(): E? {
+ return when (this) {
+ is Event.Success -> null
+ is Event.Failure -> error
+ }
+}
+
+inline fun Event.mapEither(
+ success: (V) -> U,
+ failure: (E) -> F
+): Event {
+ return when (this) {
+ is Event.Success -> Event.success(success(value))
+ is Event.Failure -> Event.error(failure(error))
+ }
+}
+
+inline fun Event.map(transform: (V) -> U): Event = try {
+ when (this) {
+ is Event.Success -> Event.Success(transform(value))
+ is Event.Failure -> Event.Failure(error)
+ }
+} catch (ex: Throwable) {
+ when (ex) {
+ is E -> Event.error(ex)
+ else -> throw ex
+ }
+}
+
+inline fun Event.flatMap(transform: (V) -> Event): Event =
+ try {
+ when (this) {
+ is Event.Success -> transform(value)
+ is Event.Failure -> Event.Failure(error)
+ }
+ } catch (ex: Throwable) {
+ when (ex) {
+ is E -> Event.error(ex)
+ else -> throw ex
+ }
+ }
+
+inline fun Event.mapError(transform: (E) -> E2) = when (this) {
+ is Event.Success -> Event.Success(value)
+ is Event.Failure -> Event.Failure(transform(error))
+}
+
+inline fun Event.flatMapError(transform: (E) -> Event) =
+ when (this) {
+ is Event.Success -> Event.Success(value)
+ is Event.Failure -> transform(error)
+ }
+
+inline fun Event.onError(f: (E) -> Unit) = when (this) {
+ is Event.Success -> Event.Success(value)
+ is Event.Failure -> {
+ f(error)
+ this
+ }
+}
+
+inline fun Event.onSuccess(f: (V) -> Unit): Event {
+ return when (this) {
+ is Event.Success -> {
+ f(value)
+ this
+ }
+ is Event.Failure -> this
+ }
+}
+
+inline fun Event.any(predicate: (V) -> Boolean): Boolean = try {
+ when (this) {
+ is Event.Success -> predicate(value)
+ is Event.Failure -> false
+ }
+} catch (ex: Throwable) {
+ false
+}
+
+inline fun Event.fanout(other: () -> Event): Event, *> =
+ flatMap { outer -> other().map { outer to it } }
+
+inline fun List>.lift(): Event, E> = fold(
+ Event.success(
+ mutableListOf()
+ ) as Event, E>
+) { acc, Event ->
+ acc.flatMap { combine ->
+ Event.map { combine.apply { add(it) } }
+ }
+}
+
+inline fun Event.unwrap(failure: (E) -> Nothing): V =
+ apply { component2()?.let(failure) }.component1()!!
+
+inline fun Event.unwrapError(success: (V) -> Nothing): E =
+ apply { component1()?.let(success) }.component2()!!
+
+
+sealed class Event: ReadOnlyProperty {
+
+ open operator fun component1(): V? = null
+ open operator fun component2(): E? = null
+
+ inline fun fold(success: (V) -> X, failure: (E) -> X): X = when (this) {
+ is Success -> success(this.value)
+ is Failure -> failure(this.error)
+ }
+
+ abstract val value: V
+
+ class Success(override val value: V) : Event() {
+ override fun component1(): V? = value
+
+ override fun toString() = "[Success: $value]"
+
+ override fun hashCode(): Int = value.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is Success<*> && value == other.value
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
+ }
+
+ class Failure(val error: E) : Event() {
+ override fun component2(): E = error
+
+ override val value: Nothing get() = throw error
+
+ fun getThrowable(): E = error
+
+ override fun toString() = "[Failure: $error]"
+
+ override fun hashCode(): Int = error.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is Failure<*> && error == other.error
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Nothing = value
+ }
+
+ companion object {
+ // Factory methods
+ fun error(ex: E) = Failure(ex)
+
+ fun success(v: V) = Success(v)
+
+ inline fun of(
+ value: V?,
+ fail: (() -> Throwable) = { Throwable() }
+ ): Event =
+ value?.let { success(it) } ?: error(fail())
+
+ inline fun of(crossinline f: () -> V): Event = try {
+ success(f())
+ } catch (ex: Throwable) {
+ when (ex) {
+ is E -> error(ex)
+ else -> throw ex
+ }
+ }
+
+ inline operator fun invoke(crossinline f: () -> V): Event = try {
+ success(f())
+ } catch (ex: Throwable) {
+ error(ex)
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt
new file mode 100644
index 00000000..9d47d6d7
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Factory.kt
@@ -0,0 +1,17 @@
+package com.shabinder.common.models.event
+
+inline fun runCatching(block: () -> V): Event {
+ return try {
+ Event.success(block())
+ } catch (e: Throwable) {
+ Event.error(e)
+ }
+}
+
+inline infix fun T.runCatching(block: T.() -> V): Event {
+ return try {
+ Event.success(block())
+ } catch (e: Throwable) {
+ Event.error(e)
+ }
+}
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt
new file mode 100644
index 00000000..d302c935
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/Validation.kt
@@ -0,0 +1,8 @@
+package com.shabinder.common.models.event
+
+class Validation(vararg resultSequence: Event<*, E>) {
+
+ val failures: List = resultSequence.filterIsInstance>().map { it.getThrowable() }
+
+ val hasFailure = failures.isNotEmpty()
+}
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt
new file mode 100644
index 00000000..bd6485b2
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendableEvent.kt
@@ -0,0 +1,174 @@
+package com.shabinder.common.models.event.coroutines
+
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+inline fun SuspendableEvent<*, *>.getAs() = when (this) {
+ is SuspendableEvent.Success -> value as? X
+ is SuspendableEvent.Failure -> error as? X
+}
+
+suspend inline fun SuspendableEvent.success(noinline f: suspend (V) -> Unit) = fold(f, {})
+
+suspend inline fun SuspendableEvent<*, E>.failure(noinline f: suspend (E) -> Unit) = fold({}, f)
+
+infix fun SuspendableEvent.or(fallback: V) = when (this) {
+ is SuspendableEvent.Success -> this
+ else -> SuspendableEvent.Success(fallback)
+}
+
+suspend inline infix fun SuspendableEvent.getOrElse(crossinline fallback:suspend (E) -> V): V {
+ return when (this) {
+ is SuspendableEvent.Success -> value
+ is SuspendableEvent.Failure -> fallback(error)
+ }
+}
+
+fun SuspendableEvent.getOrNull(): V? {
+ return when (this) {
+ is SuspendableEvent.Success -> value
+ is SuspendableEvent.Failure -> null
+ }
+}
+
+suspend inline fun SuspendableEvent.map(
+ crossinline transform: suspend (V) -> U
+): SuspendableEvent = try {
+ when (this) {
+ is SuspendableEvent.Success -> SuspendableEvent.Success(transform(value))
+ is SuspendableEvent.Failure -> SuspendableEvent.Failure(error)
+ }
+} catch (ex: Throwable) {
+ SuspendableEvent.error(ex as E)
+}
+
+suspend inline fun SuspendableEvent.flatMap(
+ crossinline transform: suspend (V) -> SuspendableEvent
+): SuspendableEvent = try {
+ when (this) {
+ is SuspendableEvent.Success -> transform(value)
+ is SuspendableEvent.Failure -> SuspendableEvent.Failure(error)
+ }
+} catch (ex: Throwable) {
+ SuspendableEvent.error(ex as E)
+}
+
+suspend inline fun SuspendableEvent.mapError(
+ crossinline transform: suspend (E) -> E2
+) = try {
+ when (this) {
+ is SuspendableEvent.Success -> SuspendableEvent.Success(value)
+ is SuspendableEvent.Failure -> SuspendableEvent.Failure(transform(error))
+ }
+} catch (ex: Throwable) {
+ SuspendableEvent.error(ex as E)
+}
+
+suspend inline fun SuspendableEvent.flatMapError(
+ crossinline transform: suspend (E) -> SuspendableEvent
+) = try {
+ when (this) {
+ is SuspendableEvent.Success -> SuspendableEvent.Success(value)
+ is SuspendableEvent.Failure -> transform(error)
+ }
+} catch (ex: Throwable) {
+ SuspendableEvent.error(ex as E)
+}
+
+suspend inline fun SuspendableEvent.any(
+ crossinline predicate: suspend (V) -> Boolean
+): Boolean = try {
+ when (this) {
+ is SuspendableEvent.Success -> predicate(value)
+ is SuspendableEvent.Failure -> false
+ }
+} catch (ex: Throwable) {
+ false
+}
+
+suspend inline fun SuspendableEvent.fanout(
+ crossinline other: suspend () -> SuspendableEvent
+): SuspendableEvent, *> =
+ flatMap { outer -> other().map { outer to it } }
+
+
+suspend fun List>.lift(): SuspendableEvent, E> = fold(
+ SuspendableEvent.Success, E>(mutableListOf()) as SuspendableEvent, E>
+) { acc, result ->
+ acc.flatMap { combine ->
+ result.map { combine.apply { add(it) } }
+ }
+}
+
+sealed class SuspendableEvent: ReadOnlyProperty {
+
+ abstract operator fun component1(): V?
+ abstract operator fun component2(): E?
+
+ suspend inline fun fold(noinline success: suspend (V) -> X, noinline failure: suspend (E) -> X): X {
+ return when (this) {
+ is Success -> success(this.value)
+ is Failure -> failure(this.error)
+ }
+ }
+
+ abstract val value: V
+
+ class Success(override val value: V) : SuspendableEvent() {
+ override fun component1(): V? = value
+ override fun component2(): E? = null
+
+ override fun toString() = "[Success: $value]"
+
+ override fun hashCode(): Int = value.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is Success<*, *> && value == other.value
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
+ }
+
+ class Failure(val error: E) : SuspendableEvent() {
+ override fun component1(): V? = null
+ override fun component2(): E? = error
+
+ override val value: V get() = throw error
+
+ fun getThrowable(): E = error
+
+ override fun toString() = "[Failure: $error]"
+
+ override fun hashCode(): Int = error.hashCode()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ return other is Failure<*, *> && error == other.error
+ }
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): V = value
+ }
+
+ companion object {
+ // Factory methods
+ fun error(ex: E) = Failure(ex)
+
+ inline fun of(value: V?,crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent {
+ return value?.let { Success(it) } ?: error(fail())
+ }
+
+ suspend inline fun of(
+ crossinline block: suspend () -> V
+ ): SuspendableEvent = try {
+ Success(block())
+ } catch (ex: Throwable) {
+ Failure(ex as E)
+ }
+
+ suspend inline operator fun invoke(
+ crossinline block: suspend () -> V
+ ): SuspendableEvent = of(block)
+ }
+
+}
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt
new file mode 100644
index 00000000..ce29ec0d
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/event/coroutines/SuspendedValidation.kt
@@ -0,0 +1,9 @@
+package com.shabinder.common.models.event.coroutines
+
+class SuspendedValidation(vararg resultSequence: SuspendableEvent<*, E>) {
+
+ val failures: List = resultSequence.filterIsInstance>().map { it.getThrowable() }
+
+ val hasFailure = failures.isNotEmpty()
+
+}
\ No newline at end of file
diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts
index 2a63e0fd..6852ae09 100644
--- a/common/dependency-injection/build.gradle.kts
+++ b/common/dependency-injection/build.gradle.kts
@@ -32,7 +32,7 @@ kotlin {
implementation(project(":common:database"))
implementation("org.jetbrains.kotlinx:atomicfu:0.16.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
- implementation("com.russhwolf:multiplatform-settings-no-arg:0.7.7")
+ api(MultiPlatformSettings.dep)
implementation(Extras.youtubeDownloader)
implementation(Extras.fuzzyWuzzy)
implementation(MVIKotlin.rx)
diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt
index 9514dbf4..3c4bf366 100644
--- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt
+++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidActual.kt
@@ -16,7 +16,6 @@
package com.shabinder.common.di
-import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import kotlinx.coroutines.CoroutineDispatcher
@@ -25,9 +24,6 @@ import kotlinx.coroutines.Dispatchers
// IO-Dispatcher
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
-// Current Platform Info
-actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
-
actual suspend fun downloadTracks(
list: List,
fetcher: FetchPlatformQueryResult,
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 27bc641b..f6b44eee 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
@@ -22,8 +22,8 @@ import android.os.Environment
import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File
-import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
@@ -43,7 +43,7 @@ import java.net.URL
@Suppress("DEPRECATION")
actual class Dir actual constructor(
private val logger: Kermit,
- settingsPref: Settings,
+ private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@Suppress("DEPRECATION")
@@ -54,7 +54,7 @@ actual class Dir actual constructor(
actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir
// fun call in order to always access Updated Value
- actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
+ actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
File.separator + "SpotiFlyer" + File.separator
actual fun isPresent(path: String): Boolean = File(path).exists()
@@ -202,5 +202,4 @@ actual class Dir actual constructor(
private val parallelExecutor = ParallelExecutor(Dispatchers.IO)
actual val db: Database? = spotiFlyerDatabase.instance
- actual val settings: Settings = settingsPref
}
diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
similarity index 87%
rename from common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
rename to common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
index 65d67107..2a523118 100644
--- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
+++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
@@ -1,8 +1,7 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.providers.requests.saavn
import android.annotation.SuppressLint
-import io.ktor.util.InternalAPI
-import io.ktor.util.decodeBase64Bytes
+import io.ktor.util.*
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey
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
deleted file mode 100644
index cbf3079d..00000000
--- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * Copyright (c) 2021 Shabinder Singh
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.shabinder.common.di.worker
-
-import android.annotation.SuppressLint
-import android.app.DownloadManager
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.PendingIntent.FLAG_CANCEL_CURRENT
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import android.os.IBinder
-import android.os.PowerManager
-import android.util.Log
-import androidx.annotation.RequiresApi
-import androidx.core.app.NotificationCompat
-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.downloadFile
-import com.shabinder.common.di.utils.ParallelExecutor
-import com.shabinder.common.models.DownloadResult
-import com.shabinder.common.models.DownloadStatus
-import com.shabinder.common.models.Status
-import com.shabinder.common.models.TrackDetails
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-import org.koin.android.ext.android.inject
-import java.io.File
-import kotlin.coroutines.CoroutineContext
-
-class ForegroundService : Service(), CoroutineScope {
-
- private val tag: String = "Foreground Service"
- private val channelId = "ForegroundDownloaderService"
- private val notificationId = 101
- private var total = 0 // Total Downloads Requested
- private var converted = 0 // Total Files Converted
- private var downloaded = 0 // Total Files downloaded
- private var failed = 0 // Total Files failed
- private val isFinished get() = converted + failed == total
- private var isSingleDownload = false
-
- private lateinit var serviceJob: Job
- override val coroutineContext: CoroutineContext
- get() = serviceJob + Dispatchers.IO
-
- private val allTracksStatus = hashMapOf()
- private var messageList = mutableListOf("", "", "", "", "")
- private var wakeLock: PowerManager.WakeLock? = null
- private var isServiceStarted = false
- private lateinit var cancelIntent: PendingIntent
-
- private lateinit var downloadManager: DownloadManager
- private lateinit var downloadService: ParallelExecutor
- private val ytDownloader get() = fetcher.youtubeProvider.ytDownloader
- private val fetcher: FetchPlatformQueryResult by inject()
- private val logger: Kermit by inject()
- private val dir: Dir by inject()
-
- override fun onBind(intent: Intent): IBinder? = null
-
- @SuppressLint("UnspecifiedImmutableFlag")
- override fun onCreate() {
- super.onCreate()
- serviceJob = SupervisorJob()
- downloadService = ParallelExecutor(Dispatchers.IO)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- createNotificationChannel(channelId, "Downloader Service")
- }
- val intent = Intent(
- this,
- ForegroundService::class.java
- ).apply { action = "kill" }
- cancelIntent = PendingIntent.getService(this, 0, intent, FLAG_CANCEL_CURRENT)
- downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
- }
-
- @SuppressLint("WakelockTimeout")
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- // Send a notification that service is started
- Log.i(tag, "Foreground Service Started.")
- startForeground(notificationId, getNotification())
-
- intent?.let {
- when (it.action) {
- "kill" -> killService()
- "query" -> {
- val response = Intent().apply {
- action = "query_result"
- synchronized(allTracksStatus) {
- putExtra("tracks", allTracksStatus)
- }
- }
- sendBroadcast(response)
- }
- }
-
- val downloadObjects: ArrayList? = (
- it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
- "object"
- )
- )
-
- downloadObjects?.let { list ->
- downloadObjects.size.let { size ->
- total += size
- isSingleDownload = (size == 1)
- }
- list.forEach { track ->
- allTracksStatus[track.title] = DownloadStatus.Queued
- }
- updateNotification()
- downloadAllTracks(list)
- }
- }
- // Wake locks and misc tasks from here :
- return if (isServiceStarted) {
- // Service Already Started
- START_STICKY
- } else {
- isServiceStarted = true
- Log.i(tag, "Starting the foreground service task")
- wakeLock =
- (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
- newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
- acquire()
- }
- }
- START_STICKY
- }
- }
-
- /**
- * Function To Download All Tracks Available in a List
- **/
- private fun downloadAllTracks(trackList: List) {
- trackList.forEach {
- launch(Dispatchers.IO) {
- downloadService.execute {
- val url = fetcher.findMp3DownloadLink(it)
- if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
- enqueueDownload(url, it)
- } else {
- sendTrackBroadcast(Status.FAILED.name, it)
- failed++
- updateNotification()
- allTracksStatus[it.title] = DownloadStatus.Failed
- }
- }
- }
- }
- }
-
- private suspend fun enqueueDownload(url: String, track: TrackDetails) {
- // 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
- downloadFile(url).collect {
- when (it) {
- is DownloadResult.Error -> {
- launch {
- logger.d(tag) { it.message }
- removeFromNotification("Downloading ${track.title}")
- failed++
- 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 -> {
- 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" }
- downloaded++
- } catch (e: Exception) {
- // Download Failed
- logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
- failed++
- }
- removeFromNotification("Downloading ${track.title}")
- }
- }
- }
- }
-
- private fun releaseWakeLock() {
- logger.d(tag) { "Releasing Wake Lock" }
- try {
- wakeLock?.let {
- if (it.isHeld) {
- it.release()
- }
- }
- } catch (e: Exception) {
- logger.d(tag) { "Service stopped without being started: ${e.message}" }
- }
- isServiceStarted = false
- }
-
- @Suppress("SameParameterValue")
- @RequiresApi(Build.VERSION_CODES.O)
- private fun createNotificationChannel(channelId: String, channelName: String) {
- val channel = NotificationChannel(
- channelId,
- channelName, NotificationManager.IMPORTANCE_DEFAULT
- )
- channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
- val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- service.createNotificationChannel(channel)
- }
-
- /*
- * Time To Wrap UP
- * - `Clean Up` and `Stop this Foreground Service`
- * */
- private fun killService() {
- launch {
- logger.d(tag) { "Killing Self" }
- messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
- downloadService.close()
- updateNotification()
- cleanFiles(File(dir.defaultDir()), logger)
- // TODO cleanFiles(File(dir.imageCacheDir()))
- messageList = mutableListOf("", "", "", "", "")
- releaseWakeLock()
- serviceJob.cancel()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- stopForeground(true)
- stopSelf()
- } else {
- stopSelf() // System will automatically close it
- }
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (isFinished) {
- killService()
- }
- }
-
- override fun onTaskRemoved(rootIntent: Intent?) {
- super.onTaskRemoved(rootIntent)
- if (isFinished) {
- killService()
- }
- }
-
- /*
- * Create A New Notification with all the updated data
- * */
- private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run {
- setSmallIcon(R.drawable.ic_download_arrow)
- setContentTitle("Total: $total Completed:$converted Failed:$failed")
- setSilent(true)
- setStyle(
- NotificationCompat.InboxStyle().run {
- addLine(messageList[messageList.size - 1])
- addLine(messageList[messageList.size - 2])
- addLine(messageList[messageList.size - 3])
- addLine(messageList[messageList.size - 4])
- addLine(messageList[messageList.size - 5])
- }
- )
- addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent)
- build()
- }
-
- private fun addToNotification(message: String) {
- messageList.add(message)
- updateNotification()
- }
-
- private fun removeFromNotification(message: String) {
- messageList.remove(message)
- updateNotification()
- }
-
- /**
- * This is the method that can be called to update the Notification
- */
- private fun updateNotification() {
- val mNotificationManager: NotificationManager =
- getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- mNotificationManager.notify(notificationId, getNotification())
- }
-
- private fun sendTrackBroadcast(action: String, track: TrackDetails) {
- val intent = Intent().apply {
- setAction(action)
- putExtra("track", track)
- }
- this@ForegroundService.sendBroadcast(intent)
- }
-}
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
index 2ed101db..27f30a37 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
@@ -20,21 +20,13 @@ import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.databaseModule
import com.shabinder.common.database.getLogger
-import com.shabinder.common.di.audioToMp3.AudioToMp3
-import com.shabinder.common.di.providers.GaanaProvider
-import com.shabinder.common.di.providers.SaavnProvider
-import com.shabinder.common.di.providers.SpotifyProvider
-import com.shabinder.common.di.providers.YoutubeMp3
-import com.shabinder.common.di.providers.YoutubeMusic
-import com.shabinder.common.di.providers.YoutubeProvider
-import io.ktor.client.HttpClient
-import io.ktor.client.features.HttpTimeout
-import io.ktor.client.features.json.JsonFeature
-import io.ktor.client.features.json.serializer.KotlinxSerializer
-import io.ktor.client.features.logging.DEFAULT
-import io.ktor.client.features.logging.LogLevel
-import io.ktor.client.features.logging.Logger
-import io.ktor.client.features.logging.Logging
+import com.shabinder.common.di.preference.PreferenceManager
+import com.shabinder.common.di.providers.providersModule
+import io.ktor.client.*
+import io.ktor.client.features.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.json.serializer.*
+import io.ktor.client.features.logging.*
import kotlinx.serialization.json.Json
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
@@ -45,7 +37,11 @@ import kotlin.native.concurrent.ThreadLocal
fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
startKoin {
appDeclaration()
- modules(commonModule(enableNetworkLogs = enableNetworkLogs), databaseModule())
+ modules(
+ commonModule(enableNetworkLogs = enableNetworkLogs),
+ providersModule(),
+ databaseModule()
+ )
}
// Called by IOS
@@ -55,16 +51,9 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
single { Dir(get(), get(), get()) }
single { Settings() }
+ single { PreferenceManager(get()) }
single { Kermit(getLogger()) }
single { TokenStore(get(), get()) }
- single { AudioToMp3(get(), get()) }
- single { SpotifyProvider(get(), get(), get()) }
- single { GaanaProvider(get(), get(), get()) }
- single { SaavnProvider(get(), get(), get(), get()) }
- single { YoutubeProvider(get(), get(), get()) }
- single { YoutubeMp3(get(), get(), get()) }
- single { YoutubeMusic(get(), get(), get(), get(), get()) }
- single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get()) }
}
@ThreadLocal
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 ad7d8bf9..ee877010 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
@@ -17,33 +17,25 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
-import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
-import io.ktor.client.request.HttpRequestBuilder
-import io.ktor.client.request.get
-import io.ktor.client.statement.HttpStatement
-import io.ktor.http.contentLength
-import io.ktor.http.isSuccess
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt
-const val DirKey = "downloadDir"
-const val AnalyticsKey = "analytics"
-const val FirstLaunch = "firstLaunch"
-const val DonationInterval = "donationInterval"
-
expect class Dir(
logger: Kermit,
- settingsPref: Settings,
+ preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
val db: Database?
- val settings: Settings
fun isPresent(path: String): Boolean
fun fileSeparator(): String
fun defaultDir(): String
@@ -56,22 +48,6 @@ expect class Dir(
fun addToLibrary(path: String)
}
-val Dir.isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
-fun Dir.toggleAnalytics(enabled: Boolean) = settings.putBoolean(AnalyticsKey, enabled)
-
-fun Dir.setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
-
-val Dir.getDonationOffset: Int get() = (settings.getIntOrNull(DonationInterval) ?: 3).also {
- // Min. Donation Asking Interval is `3`
- if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1)
-}
-fun Dir.setDonationOffset(offset: Int = 5) = settings.putInt(DonationInterval, offset)
-
-val Dir.isFirstLaunch get() = settings.getBooleanOrNull(FirstLaunch) ?: true
-fun Dir.firstLaunchDone() {
- settings.putBoolean(FirstLaunch, false)
-}
-
/*
* Call this function at startup!
* */
@@ -105,7 +81,7 @@ suspend fun downloadFile(url: String): Flow {
var offset = 0
do {
// Set Length optimally, after how many kb you want a progress update, now it 0.25mb
- val currentRead = response.content.readAvailable(data, offset, 250000)
+ val currentRead = response.content.readAvailable(data, offset, 2_50_000)
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/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt
index 97fb7c46..1d1dfaf6 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Expect.kt
@@ -16,9 +16,8 @@
package com.shabinder.common.di
-import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails
-import io.ktor.client.request.head
+import io.ktor.client.request.*
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -34,10 +33,6 @@ expect suspend fun downloadTracks(
@SharedImmutable
expect val dispatcherIO: CoroutineDispatcher
-// Current Platform Info
-@SharedImmutable
-expect val currentPlatform: AllPlatforms
-
suspend fun isInternetAccessible(): Boolean {
return withContext(dispatcherIO) {
try {
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
index 58a842d9..b7eabb08 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
@@ -16,8 +16,8 @@
package com.shabinder.common.di
+import co.touchlab.kermit.Kermit
import com.shabinder.common.database.DownloadRecordDatabaseQueries
-import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider
@@ -25,26 +25,37 @@ import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider
import com.shabinder.common.di.providers.get
+import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
import com.shabinder.common.models.PlatformQueryResult
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.flatMap
+import com.shabinder.common.models.event.coroutines.flatMapError
+import com.shabinder.common.models.event.coroutines.success
import com.shabinder.common.models.spotify.Source
+import com.shabinder.common.requireNotNull
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class FetchPlatformQueryResult(
private val gaanaProvider: GaanaProvider,
- val spotifyProvider: SpotifyProvider,
- val youtubeProvider: YoutubeProvider,
+ private val spotifyProvider: SpotifyProvider,
+ private val youtubeProvider: YoutubeProvider,
private val saavnProvider: SaavnProvider,
- val youtubeMusic: YoutubeMusic,
- val youtubeMp3: YoutubeMp3,
- val audioToMp3: AudioToMp3,
- val dir: Dir
+ private val youtubeMusic: YoutubeMusic,
+ private val youtubeMp3: YoutubeMp3,
+ private val audioToMp3: AudioToMp3,
+ val dir: Dir,
+ val logger: Kermit
) {
private val db: DownloadRecordDatabaseQueries?
get() = dir.db?.downloadRecordDatabaseQueries
- suspend fun query(link: String): PlatformQueryResult? {
+ suspend fun authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient()
+
+ suspend fun query(link: String): SuspendableEvent {
val result = when {
// SPOTIFY
link.contains("spotify", true) ->
@@ -63,13 +74,13 @@ class FetchPlatformQueryResult(
gaanaProvider.query(link)
else -> {
- null
+ SuspendableEvent.error(SpotiFlyerException.LinkInvalid(link))
}
}
- if (result != null) {
+ result.success {
addToDatabaseAsync(
link,
- result.copy() // Send a copy in order to not to freeze Result itself
+ it.copy() // Send a copy in order to not to freeze Result itself
)
}
return result
@@ -79,35 +90,55 @@ class FetchPlatformQueryResult(
// 2) If Not found try finding on Youtube Music
suspend fun findMp3DownloadLink(
track: TrackDetails
- ): String? =
+ ): SuspendableEvent =
if (track.videoID != null) {
// We Already have VideoID
when (track.source) {
Source.JioSaavn -> {
- saavnProvider.getSongFromID(track.videoID!!).media_url?.let { m4aLink ->
- audioToMp3.convertToMp3(m4aLink)
+ saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song ->
+ song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findHighestQualityMp3Link(track)
}
}
Source.YouTube -> {
- youtubeMp3.getMp3DownloadLink(track.videoID!!)
- ?: youtubeProvider.ytDownloader?.getVideo(track.videoID!!)?.get()?.url?.let { m4aLink ->
+ youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull()).flatMapError {
+ youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink ->
audioToMp3.convertToMp3(m4aLink)
- }
+ } ?: throw SpotiFlyerException.YoutubeLinkNotFound(track.videoID)
+ }
}
else -> {
- null/* Do Nothing, We should never reach here for now*/
+ /*We should never reach here for now*/
+ findHighestQualityMp3Link(track)
}
}
} else {
- // First Try Getting A Link From JioSaavn
- saavnProvider.findSongDownloadURL(
- trackName = track.title,
- trackArtists = track.artists
- )
- // Lets Try Fetching Now From Youtube Music
- ?: youtubeMusic.findSongDownloadURL(track)
+ findHighestQualityMp3Link(track)
}
+ private suspend fun findHighestQualityMp3Link(
+ track: TrackDetails
+ ):SuspendableEvent {
+ // Try Fetching Track from Jio Saavn
+ return saavnProvider.findMp3SongDownloadURL(
+ trackName = track.title,
+ trackArtists = track.artists
+ ).flatMapError { saavnError ->
+ logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
+ // Saavn Failed, Lets Try Fetching Now From Youtube Music
+ youtubeMusic.findMp3SongDownloadURLYT(track).flatMapError { ytMusicError ->
+ // If Both Failed Bubble the Exception Up with both StackTraces
+ SuspendableEvent.error(
+ SpotiFlyerException.DownloadLinkFetchFailed(
+ trackName = track.title,
+ jioSaavnError = saavnError,
+ ytMusicError = ytMusicError
+ )
+ )
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
GlobalScope.launch(dispatcherIO) {
db?.add(
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt
index 0d139628..0526e306 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/TokenStore.kt
@@ -18,7 +18,7 @@ package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import com.shabinder.common.database.TokenDBQueries
-import com.shabinder.common.di.spotify.authenticateSpotify
+import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify
import com.shabinder.common.models.spotify.TokenData
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -43,7 +43,7 @@ class TokenStore(
logger.d { "System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}" }
if (Clock.System.now().epochSeconds > token?.expiry ?: 0 || token == null) {
logger.d { "Requesting New Token" }
- token = authenticateSpotify()
+ token = authenticateSpotify().component1()
GlobalScope.launch { token?.access_token?.let { save(token) } }
}
return token
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt
new file mode 100644
index 00000000..3e44ba91
--- /dev/null
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/preference/PreferenceManager.kt
@@ -0,0 +1,35 @@
+package com.shabinder.common.di.preference
+
+import com.russhwolf.settings.Settings
+
+class PreferenceManager(settings: Settings): Settings by settings {
+
+ companion object {
+ const val DirKey = "downloadDir"
+ const val AnalyticsKey = "analytics"
+ const val FirstLaunch = "firstLaunch"
+ const val DonationInterval = "donationInterval"
+ }
+
+ /* ANALYTICS */
+ val isAnalyticsEnabled get() = getBooleanOrNull(AnalyticsKey) ?: false
+ fun toggleAnalytics(enabled: Boolean) = putBoolean(AnalyticsKey, enabled)
+
+
+ /* DOWNLOAD DIRECTORY */
+ val downloadDir get() = getStringOrNull(DirKey)
+ fun setDownloadDirectory(newBasePath: String) = putString(DirKey, newBasePath)
+
+
+ /* OFFSET FOR WHEN TO ASK FOR SUPPORT */
+ val getDonationOffset: Int get() = (getIntOrNull(DonationInterval) ?: 3).also {
+ // Min. Donation Asking Interval is `3`
+ if (it < 3) setDonationOffset(3) else setDonationOffset(it - 1)
+ }
+ fun setDonationOffset(offset: Int = 5) = putInt(DonationInterval, offset)
+
+
+ /* TO CHECK IF THIS IS APP's FIRST LAUNCH */
+ val isFirstLaunch get() = getBooleanOrNull(FirstLaunch) ?: true
+ fun firstLaunchDone() = putBoolean(FirstLaunch, false)
+}
\ No newline at end of file
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt
index 476ca9b0..37d27c87 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt
@@ -19,13 +19,15 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.finalOutputDir
-import com.shabinder.common.di.gaana.GaanaRequests
+import com.shabinder.common.di.providers.requests.gaana.GaanaRequests
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.gaana.GaanaTrack
import com.shabinder.common.models.spotify.Source
-import io.ktor.client.HttpClient
+import io.ktor.client.*
class GaanaProvider(
override val httpClient: HttpClient,
@@ -35,7 +37,7 @@ class GaanaProvider(
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
- suspend fun query(fullLink: String): PlatformQueryResult? {
+ suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent {
// Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")
@@ -44,17 +46,13 @@ class GaanaProvider(
// Error
if (type == "Error" || link == "Error") {
- return null
- }
- return try {
- gaanaSearch(
- type,
- link
- )
- } catch (e: Exception) {
- e.printStackTrace()
- null
+ throw SpotiFlyerException.LinkInvalid()
}
+
+ gaanaSearch(
+ type,
+ link
+ )
}
private suspend fun gaanaSearch(
@@ -137,6 +135,7 @@ class GaanaProvider(
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
)
}
+
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
return if (dir.isPresent(
dir.finalOutputDir(
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt
new file mode 100644
index 00000000..b4d46549
--- /dev/null
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/ProvidersModule.kt
@@ -0,0 +1,16 @@
+package com.shabinder.common.di.providers
+
+import com.shabinder.common.di.FetchPlatformQueryResult
+import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
+import org.koin.dsl.module
+
+fun providersModule() = module {
+ single { AudioToMp3(get(), get()) }
+ single { SpotifyProvider(get(), get(), get()) }
+ single { GaanaProvider(get(), get(), get()) }
+ single { SaavnProvider(get(), get(), get(), get()) }
+ single { YoutubeProvider(get(), get(), get()) }
+ single { YoutubeMp3(get(), get()) }
+ single { YoutubeMusic(get(), get(), get(), get(), get()) }
+ single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
+}
\ No newline at end of file
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
index 290744b7..7188ada4 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
@@ -2,16 +2,18 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
-import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.finalOutputDir
-import com.shabinder.common.di.saavn.JioSaavnRequests
+import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
+import com.shabinder.common.di.providers.requests.saavn.JioSaavnRequests
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.saavn.SaavnSong
import com.shabinder.common.models.spotify.Source
-import io.ktor.client.HttpClient
+import io.ktor.client.*
class SaavnProvider(
override val httpClient: HttpClient,
@@ -20,19 +22,18 @@ class SaavnProvider(
private val dir: Dir,
) : JioSaavnRequests {
- suspend fun query(fullLink: String): PlatformQueryResult {
- val result = PlatformQueryResult(
+ suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent {
+ PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.JioSaavn
- )
- with(result) {
+ ).apply {
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
"song" -> {
- getSong(fullLink).let {
+ getSong(fullLink).value.let {
folderType = "Tracks"
subFolder = ""
trackList = listOf(it).toTrackDetails(folderType, subFolder)
@@ -41,7 +42,7 @@ class SaavnProvider(
}
}
"album" -> {
- getAlbum(fullLink)?.let {
+ getAlbum(fullLink).value.let {
folderType = "Albums"
subFolder = removeIllegalChars(it.title)
trackList = it.songs.toTrackDetails(folderType, subFolder)
@@ -50,7 +51,7 @@ class SaavnProvider(
}
}
"featured" -> { // Playlist
- getPlaylist(fullLink)?.let {
+ getPlaylist(fullLink).value.let {
folderType = "Playlists"
subFolder = removeIllegalChars(it.listname)
trackList = it.songs.toTrackDetails(folderType, subFolder)
@@ -59,12 +60,10 @@ class SaavnProvider(
}
}
else -> {
- // Handle Error
+ throw SpotiFlyerException.LinkInvalid(fullLink)
}
}
}
-
- return result
}
private fun List.toTrackDetails(type: String, subFolder: String): List = this.map {
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt
index 44b67370..0d446611 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt
@@ -22,22 +22,24 @@ import com.shabinder.common.di.TokenStore
import com.shabinder.common.di.createHttpClient
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.globalJson
-import com.shabinder.common.di.spotify.SpotifyRequests
-import com.shabinder.common.di.spotify.authenticateSpotify
+import com.shabinder.common.di.providers.requests.spotify.SpotifyRequests
+import com.shabinder.common.di.providers.requests.spotify.authenticateSpotify
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.NativeAtomicReference
import com.shabinder.common.models.PlatformQueryResult
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.spotify.Album
import com.shabinder.common.models.spotify.Image
import com.shabinder.common.models.spotify.PlaylistTrack
import com.shabinder.common.models.spotify.Source
import com.shabinder.common.models.spotify.Track
-import io.ktor.client.HttpClient
-import io.ktor.client.features.defaultRequest
-import io.ktor.client.features.json.JsonFeature
-import io.ktor.client.features.json.serializer.KotlinxSerializer
-import io.ktor.client.request.header
+import io.ktor.client.*
+import io.ktor.client.features.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.json.serializer.*
+import io.ktor.client.request.*
class SpotifyProvider(
private val tokenStore: TokenStore,
@@ -46,9 +48,9 @@ class SpotifyProvider(
) : SpotifyRequests {
override suspend fun authenticateSpotifyClient(override: Boolean) {
- val token = if (override) authenticateSpotify() else tokenStore.getToken()
+ val token = if (override) authenticateSpotify().component1() else tokenStore.getToken()
if (token == null) {
- logger.d { "Please Check your Network Connection" }
+ logger.d { "Spotify Auth Failed: Please Check your Network Connection" }
} else {
logger.d { "Spotify Provider Created with $token" }
HttpClient {
@@ -64,7 +66,7 @@ class SpotifyProvider(
override val httpClientRef = NativeAtomicReference(createHttpClient(true))
- suspend fun query(fullLink: String): PlatformQueryResult? {
+ suspend fun query(fullLink: String): SuspendableEvent = SuspendableEvent {
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
@@ -78,15 +80,16 @@ class SpotifyProvider(
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
if (type == "Error" || link == "Error") {
- return null
+ throw SpotiFlyerException.LinkInvalid(fullLink)
}
if (type == "episode" || type == "show") {
- // TODO Implementation
- return null
+ throw SpotiFlyerException.FeatureNotImplementedYet(
+ "Support for Spotify's ${type.uppercase()} isn't implemented yet"
+ )
}
- return try {
+ try {
spotifySearch(
type,
link
@@ -95,16 +98,11 @@ class SpotifyProvider(
e.printStackTrace()
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
authenticateSpotifyClient(true)
- // Retry Search
- try {
- spotifySearch(
- type,
- link
- )
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+
+ spotifySearch(
+ type,
+ link
+ )
}
}
@@ -112,15 +110,14 @@ class SpotifyProvider(
type: String,
link: String
): PlatformQueryResult {
- val result = PlatformQueryResult(
+ return PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.Spotify
- )
- with(result) {
+ ).apply {
when (type) {
"track" -> {
getTrack(link).also {
@@ -186,15 +183,16 @@ class SpotifyProvider(
coverUrl = playlistObject.images?.firstOrNull()?.url.toString()
}
"episode" -> { // TODO
+ throw SpotiFlyerException.FeatureNotImplementedYet()
}
"show" -> { // TODO
+ throw SpotiFlyerException.FeatureNotImplementedYet()
}
else -> {
- // TODO Handle Error
+ throw SpotiFlyerException.LinkInvalid("Provide: Spotify, Type:$type -> Link:$link")
}
}
}
- return result
}
/*
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt
index 9bd488ea..5ba98a5c 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt
@@ -17,28 +17,27 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
-import com.shabinder.common.di.Dir
-import com.shabinder.common.di.currentPlatform
-import com.shabinder.common.di.youtubeMp3.Yt1sMp3
-import com.shabinder.common.models.AllPlatforms
+import com.shabinder.common.di.providers.requests.youtubeMp3.Yt1sMp3
+import com.shabinder.common.models.corsApi
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.map
import io.ktor.client.*
-class YoutubeMp3(
- override val httpClient: HttpClient,
- override val logger: Kermit,
- private val dir: Dir,
-) : Yt1sMp3 {
- suspend fun getMp3DownloadLink(videoID: String): String? = try {
- logger.i { "Youtube MP3 Link Fetching!" }
- getLinkFromYt1sMp3(videoID)?.let {
- logger.i { "Download Link: $it" }
- if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
- "https://cors.spotiflyer.ml/cors/$it"
- // "https://spotiflyer.azurewebsites.net/$it" // Data OUT Limit issue
- else it
+interface YoutubeMp3: Yt1sMp3 {
+
+ companion object {
+ operator fun invoke(
+ client: HttpClient,
+ logger: Kermit
+ ): YoutubeMp3 {
+ return object : YoutubeMp3 {
+ override val httpClient: HttpClient = client
+ override val logger: Kermit = logger
+ }
}
- } catch (e: Exception) {
- e.printStackTrace()
- null
+ }
+
+ suspend fun getMp3DownloadLink(videoID: String): SuspendableEvent = getLinkFromYt1sMp3(videoID).map {
+ corsApi + it
}
}
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
index 215a5ae5..0949226e 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
@@ -17,16 +17,19 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
-import com.shabinder.common.di.audioToMp3.AudioToMp3
-import com.shabinder.common.di.gaana.corsApi
+import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
+import com.shabinder.common.models.corsApi
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.flatMap
+import com.shabinder.common.models.event.coroutines.flatMapError
+import com.shabinder.common.models.event.coroutines.map
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
-import io.ktor.client.HttpClient
-import io.ktor.client.request.headers
-import io.ktor.client.request.post
-import io.ktor.http.ContentType
-import io.ktor.http.contentType
+import io.ktor.client.*
+import io.ktor.client.request.*
+import io.ktor.http.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.buildJsonArray
@@ -37,196 +40,197 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
+import kotlin.collections.set
import kotlin.math.absoluteValue
class YoutubeMusic constructor(
private val logger: Kermit,
private val httpClient: HttpClient,
- private val youtubeMp3: YoutubeMp3,
private val youtubeProvider: YoutubeProvider,
+ private val youtubeMp3: YoutubeMp3,
private val audioToMp3: AudioToMp3
) {
-
companion object {
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
const val tag = "YT Music"
}
- suspend fun findSongDownloadURL(
+ // Get Downloadable Link
+ suspend fun findMp3SongDownloadURLYT(
trackDetails: TrackDetails
- ): String? {
- val bestMatchVideoID = getYTIDBestMatch(trackDetails)
- return bestMatchVideoID?.let { videoID ->
- youtubeMp3.getMp3DownloadLink(videoID) ?: youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url?.let { m4aLink ->
- audioToMp3.convertToMp3(
- m4aLink
+ ): SuspendableEvent {
+ return getYTIDBestMatch(trackDetails).flatMap { videoID ->
+ // 1 Try getting Link from Yt1s
+ youtubeMp3.getMp3DownloadLink(videoID).flatMapError {
+ // 2 if Yt1s failed , Extract Manually
+ youtubeProvider.ytDownloader.getVideo(videoID).get()?.url?.let { m4aLink ->
+ audioToMp3.convertToMp3(m4aLink)
+ } ?: throw SpotiFlyerException.YoutubeLinkNotFound(
+ videoID,
+ message = "Caught Following Errors While Finding Downloadable Link for $videoID : \n${it.stackTraceToString()}"
)
}
}
}
- suspend fun getYTIDBestMatch(
+ private suspend fun getYTIDBestMatch(
trackDetails: TrackDetails
- ): String? {
- return try {
+ ):SuspendableEvent =
+ getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList ->
sortByBestMatch(
- getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}"),
+ matchList,
trackName = trackDetails.title,
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
- ).keys.firstOrNull()
- } catch (e: Exception) {
- // All Internet/Client Related Errors
- e.printStackTrace()
- null
+ ).keys.firstOrNull() ?: throw SpotiFlyerException.NoMatchFound(trackDetails.title)
}
- }
- private suspend fun getYTTracks(query: String): List {
- val youtubeTracks = mutableListOf()
- val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
- logger.i { "Youtube Music Response Recieved" }
- val contentBlocks = responseObj.jsonObject["contents"]
- ?.jsonObject?.get("sectionListRenderer")
- ?.jsonObject?.get("contents")?.jsonArray
+ private suspend fun getYTTracks(query: String): SuspendableEvent,Throwable> =
+ getYoutubeMusicResponse(query).map { youtubeResponseData ->
+ val youtubeTracks = mutableListOf()
+ val responseObj = Json.parseToJsonElement(youtubeResponseData)
+ // logger.i { "Youtube Music Response Received" }
+ val contentBlocks = responseObj.jsonObject["contents"]
+ ?.jsonObject?.get("sectionListRenderer")
+ ?.jsonObject?.get("contents")?.jsonArray
- val resultBlocks = mutableListOf()
- if (contentBlocks != null) {
- for (cBlock in contentBlocks) {
- /**
- *Ignore user-suggestion
- *The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
- *results for xyz, search for abc instead') we have no use for them, the for
- *loop below if throw a keyError if we don't ignore them
- */
- if (cBlock.jsonObject.containsKey("itemSectionRenderer")) {
- continue
- }
-
- for (
- contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray
- ?: listOf()
- ) {
+ val resultBlocks = mutableListOf()
+ if (contentBlocks != null) {
+ for (cBlock in contentBlocks) {
/**
- * apparently content Blocks without an 'overlay' field don't have linkBlocks
- * I have no clue what they are and why there even exist
- *
- if(!contents.containsKey("overlay")){
- println(contents)
- continue
- TODO check and correct
- }*/
-
- val result = contents.jsonObject["musicResponsiveListItemRenderer"]
- ?.jsonObject?.get("flexColumns")?.jsonArray
-
- // Add the linkBlock
- val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"]
- ?.jsonObject?.get("overlay")
- ?.jsonObject?.get("musicItemThumbnailOverlayRenderer")
- ?.jsonObject?.get("content")
- ?.jsonObject?.get("musicPlayButtonRenderer")
- ?.jsonObject?.get("playNavigationEndpoint")
-
- // detailsBlock is always a list, so we just append the linkBlock to it
- // instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
- val finalResult = buildJsonArray {
- result?.let { add(it) }
- linkBlock?.let { add(it) }
+ *Ignore user-suggestion
+ *The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
+ *results for xyz, search for abc instead') we have no use for them, the for
+ *loop below if throw a keyError if we don't ignore them
+ */
+ if (cBlock.jsonObject.containsKey("itemSectionRenderer")) {
+ continue
+ }
+
+ for (
+ contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray
+ ?: listOf()
+ ) {
+ /**
+ * apparently content Blocks without an 'overlay' field don't have linkBlocks
+ * I have no clue what they are and why there even exist
+ *
+ if(!contents.containsKey("overlay")){
+ println(contents)
+ continue
+ TODO check and correct
+ }*/
+
+ val result = contents.jsonObject["musicResponsiveListItemRenderer"]
+ ?.jsonObject?.get("flexColumns")?.jsonArray
+
+ // Add the linkBlock
+ val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"]
+ ?.jsonObject?.get("overlay")
+ ?.jsonObject?.get("musicItemThumbnailOverlayRenderer")
+ ?.jsonObject?.get("content")
+ ?.jsonObject?.get("musicPlayButtonRenderer")
+ ?.jsonObject?.get("playNavigationEndpoint")
+
+ // detailsBlock is always a list, so we just append the linkBlock to it
+ // instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
+ val finalResult = buildJsonArray {
+ result?.let { add(it) }
+ linkBlock?.let { add(it) }
+ }
+ resultBlocks.add(finalResult)
}
- resultBlocks.add(finalResult)
}
- }
- /* We only need results that are Songs or Videos, so we filter out the rest, since
- ! Songs and Videos are supplied with different details, extracting all details from
- ! both is just carrying on redundant data, so we also have to selectively extract
- ! relevant details. What you need to know to understand how we do that here:
- !
- ! Songs details are ALWAYS in the following order:
- ! 0 - Name
- ! 1 - Type (Song)
- ! 2 - com.shabinder.spotiflyer.models.gaana.Artist
- ! 3 - Album
- ! 4 - Duration (mm:ss)
- !
- ! Video details are ALWAYS in the following order:
- ! 0 - Name
- ! 1 - Type (Video)
- ! 2 - Channel
- ! 3 - Viewers
- ! 4 - Duration (hh:mm:ss)
- !
- ! We blindly gather all the details we get our hands on, then
- ! cherry pick the details we need based on their index numbers,
- ! we do so only if their Type is 'Song' or 'Video
- */
-
- for (result in resultBlocks) {
-
- // Blindly gather available details
- val availableDetails = mutableListOf()
-
- /*
- Filter Out dummies here itself
- ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
- ! sub-block, if not its a dummy, why does the YTM response contain dummies?
- ! I have no clue. We skip these.
-
- ! Remember that we appended the linkBlock to result, treating that like the
- ! other constituents of a result block will lead to errors, hence the 'in
- ! result[:-1] ,i.e., skip last element in array '
+ /* We only need results that are Songs or Videos, so we filter out the rest, since
+ ! Songs and Videos are supplied with different details, extracting all details from
+ ! both is just carrying on redundant data, so we also have to selectively extract
+ ! relevant details. What you need to know to understand how we do that here:
+ !
+ ! Songs details are ALWAYS in the following order:
+ ! 0 - Name
+ ! 1 - Type (Song)
+ ! 2 - com.shabinder.spotiflyer.models.gaana.Artist
+ ! 3 - Album
+ ! 4 - Duration (mm:ss)
+ !
+ ! Video details are ALWAYS in the following order:
+ ! 0 - Name
+ ! 1 - Type (Video)
+ ! 2 - Channel
+ ! 3 - Viewers
+ ! 4 - Duration (hh:mm:ss)
+ !
+ ! We blindly gather all the details we get our hands on, then
+ ! cherry pick the details we need based on their index numbers,
+ ! we do so only if their Type is 'Song' or 'Video
*/
- for (detailArray in result.subList(0, result.size - 1)) {
- for (detail in detailArray.jsonArray) {
- if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue
- // if not a dummy, collect All Variables
- val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]
- ?.jsonObject?.get("text")
- ?.jsonObject?.get("runs")?.jsonArray ?: listOf()
+ for (result in resultBlocks) {
- for (d in details) {
- d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let {
- if (it != " • ") {
- availableDetails.add(it)
+ // Blindly gather available details
+ val availableDetails = mutableListOf()
+
+ /*
+ Filter Out dummies here itself
+ ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
+ ! sub-block, if not its a dummy, why does the YTM response contain dummies?
+ ! I have no clue. We skip these.
+
+ ! Remember that we appended the linkBlock to result, treating that like the
+ ! other constituents of a result block will lead to errors, hence the 'in
+ ! result[:-1] ,i.e., skip last element in array '
+ */
+ for (detailArray in result.subList(0, result.size - 1)) {
+ for (detail in detailArray.jsonArray) {
+ if (detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size ?: 0 < 2) continue
+
+ // if not a dummy, collect All Variables
+ val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]
+ ?.jsonObject?.get("text")
+ ?.jsonObject?.get("runs")?.jsonArray ?: listOf()
+
+ for (d in details) {
+ d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let {
+ if (it != " • ") {
+ availableDetails.add(it)
+ }
}
}
}
}
- }
- // logger.d("YT Music details"){availableDetails.toString()}
- /*
- ! Filter Out non-Song/Video results and incomplete results here itself
- ! From what we know about detail order, note that [1] - indicate result type
- */
- if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) {
-
- // skip if result is in hours instead of minutes (no song is that long)
- if (availableDetails[4].split(':').size != 2) continue
-
+ // logger.d("YT Music details"){availableDetails.toString()}
/*
- ! grab Video ID
- ! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
- ! so hardcoding the dict keys for data look up is an ardours process, since
- ! the sub-block pattern is fixed even though the key isn't, we just
- ! reference the dict keys by index
+ ! Filter Out non-Song/Video results and incomplete results here itself
+ ! From what we know about detail order, note that [1] - indicate result type
*/
+ if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) {
- val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content
- val ytTrack = YoutubeTrack(
- name = availableDetails[0],
- type = availableDetails[1],
- artist = availableDetails[2],
- duration = availableDetails[4],
- videoId = videoId
- )
- youtubeTracks.add(ytTrack)
+ // skip if result is in hours instead of minutes (no song is that long)
+ if (availableDetails[4].split(':').size != 2) continue
+
+ /*
+ ! grab Video ID
+ ! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
+ ! so hardcoding the dict keys for data look up is an ardours process, since
+ ! the sub-block pattern is fixed even though the key isn't, we just
+ ! reference the dict keys by index
+ */
+
+ val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content
+ val ytTrack = YoutubeTrack(
+ name = availableDetails[0],
+ type = availableDetails[1],
+ artist = availableDetails[2],
+ duration = availableDetails[4],
+ videoId = videoId
+ )
+ youtubeTracks.add(ytTrack)
+ }
}
}
- }
// logger.d {youtubeTracks.joinToString("\n")}
- return youtubeTracks
+ youtubeTracks
}
private fun sortByBestMatch(
@@ -246,8 +250,8 @@ class YoutubeMusic constructor(
// most song results on youtube go by $artist - $songName or artist1/artist2
var hasCommonWord = false
- val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: ""
- val trackNameWords = trackName.toLowerCase().split(" ")
+ val resultName = result.name?.lowercase()?.replace("-", " ")?.replace("/", " ") ?: ""
+ val trackNameWords = trackName.lowercase().split(" ")
for (nameWord in trackNameWords) {
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
@@ -266,12 +270,12 @@ class YoutubeMusic constructor(
if (result.type == "Song") {
for (artist in trackArtists) {
- if (FuzzySearch.ratio(artist.toLowerCase(), result.artist?.toLowerCase() ?: "") > 85)
+ if (FuzzySearch.ratio(artist.lowercase(), result.artist?.lowercase() ?: "") > 85)
artistMatchNumber++
}
} else { // i.e. is a Video
for (artist in trackArtists) {
- if (FuzzySearch.partialRatio(artist.toLowerCase(), result.name?.toLowerCase() ?: "") > 85)
+ if (FuzzySearch.partialRatio(artist.lowercase(), result.name?.lowercase() ?: "") > 85)
artistMatchNumber++
}
}
@@ -303,9 +307,8 @@ class YoutubeMusic constructor(
}
}
- private suspend fun getYoutubeMusicResponse(query: String): String {
- logger.i { "Fetching Youtube Music Response" }
- return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
+ private suspend fun getYoutubeMusicResponse(query: String): SuspendableEvent = SuspendableEvent {
+ httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
contentType(ContentType.Application.Json)
headers {
append("referer", "https://music.youtube.com/search")
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt
index 6f720ac8..8ae28336 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt
@@ -22,7 +22,9 @@ import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.spotify.Source
import io.github.shabinder.YoutubeDownloader
import io.github.shabinder.models.YoutubeVideo
@@ -49,7 +51,7 @@ class YoutubeProvider(
private val sampleDomain2 = "youtube.com"
private val sampleDomain3 = "youtu.be"
- suspend fun query(fullLink: String): PlatformQueryResult? {
+ suspend fun query(fullLink: String): SuspendableEvent {
val link = fullLink.removePrefix("https://").removePrefix("http://")
if (link.contains("playlist", true) || link.contains("list", true)) {
// Given Link is of a Playlist
@@ -77,74 +79,15 @@ class YoutubeProvider(
)
} else {
logger.d { "Your Youtube Link is not of a Video!!" }
- null
+ SuspendableEvent.error(SpotiFlyerException.LinkInvalid(fullLink))
}
}
}
private suspend fun getYTPlaylist(
searchId: String
- ): PlatformQueryResult? {
- val result = PlatformQueryResult(
- folderType = "",
- subFolder = "",
- title = "",
- coverUrl = "",
- trackList = listOf(),
- Source.YouTube
- )
- result.apply {
- try {
- val playlist = ytDownloader.getPlaylist(searchId)
- val playlistDetails = playlist.details
- val name = playlistDetails.title
- subFolder = removeIllegalChars(name)
- val videos = playlist.videos
-
- coverUrl = "https://i.ytimg.com/vi/${
- videos.firstOrNull()?.videoId
- }/hqdefault.jpg"
- title = name
-
- trackList = videos.map {
- TrackDetails(
- title = it.title ?: "N/A",
- artists = listOf(it.author ?: "N/A"),
- durationSec = it.lengthSeconds,
- albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
- source = Source.YouTube,
- albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
- downloaded = if (dir.isPresent(
- dir.finalOutputDir(
- itemName = it.title ?: "N/A",
- type = folderType,
- subFolder = subFolder,
- dir.defaultDir()
- )
- )
- )
- DownloadStatus.Downloaded
- else {
- DownloadStatus.NotDownloaded
- },
- outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
- videoID = it.videoId
- )
- }
- } catch (e: Exception) {
- e.printStackTrace()
- logger.d { "An Error Occurred While Processing!" }
- }
- }
- return if (result.title.isNotBlank()) result
- else null
- }
-
- @Suppress("DefaultLocale")
- private suspend fun getYTTrack(
- searchId: String,
- ): PlatformQueryResult? {
- val result = PlatformQueryResult(
+ ): SuspendableEvent = SuspendableEvent {
+ PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
@@ -152,47 +95,90 @@ class YoutubeProvider(
trackList = listOf(),
Source.YouTube
).apply {
- try {
- logger.i { searchId }
- val video = ytDownloader.getVideo(searchId)
- coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
- val detail = video.videoDetails
- val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true)
- ?: detail.title ?: ""
- // logger.i{ detail.toString() }
- trackList = listOf(
- TrackDetails(
- title = name,
- artists = listOf(detail.author ?: "N/A"),
- durationSec = detail.lengthSeconds,
- albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
- source = Source.YouTube,
- albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
- downloaded = if (dir.isPresent(
- dir.finalOutputDir(
- itemName = name,
- type = folderType,
- subFolder = subFolder,
- defaultDir = dir.defaultDir()
- )
+ val playlist = ytDownloader.getPlaylist(searchId)
+ val playlistDetails = playlist.details
+ val name = playlistDetails.title
+ subFolder = removeIllegalChars(name)
+ val videos = playlist.videos
+
+ coverUrl = "https://i.ytimg.com/vi/${
+ videos.firstOrNull()?.videoId
+ }/hqdefault.jpg"
+ title = name
+
+ trackList = videos.map {
+ TrackDetails(
+ title = it.title ?: "N/A",
+ artists = listOf(it.author ?: "N/A"),
+ durationSec = it.lengthSeconds,
+ albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
+ source = Source.YouTube,
+ albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
+ downloaded = if (dir.isPresent(
+ dir.finalOutputDir(
+ itemName = it.title ?: "N/A",
+ type = folderType,
+ subFolder = subFolder,
+ dir.defaultDir()
)
)
- DownloadStatus.Downloaded
- else {
- DownloadStatus.NotDownloaded
- },
- outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
- videoID = searchId
)
+ DownloadStatus.Downloaded
+ else {
+ DownloadStatus.NotDownloaded
+ },
+ outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
+ videoID = it.videoId
)
- title = name
- } catch (e: Exception) {
- e.printStackTrace()
- logger.e { "An Error Occurred While Processing!,$searchId" }
}
}
- return if (result.title.isNotBlank()) result
- else null
+ }
+
+ @Suppress("DefaultLocale")
+ private suspend fun getYTTrack(
+ searchId: String,
+ ): SuspendableEvent = SuspendableEvent {
+ PlatformQueryResult(
+ folderType = "",
+ subFolder = "",
+ title = "",
+ coverUrl = "",
+ trackList = listOf(),
+ Source.YouTube
+ ).apply {
+ val video = ytDownloader.getVideo(searchId)
+ coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
+ val detail = video.videoDetails
+ val name = detail.title?.replace(detail.author?.uppercase() ?: "", "", true)
+ ?: detail.title ?: ""
+ // logger.i{ detail.toString() }
+ trackList = listOf(
+ TrackDetails(
+ title = name,
+ artists = listOf(detail.author ?: "N/A"),
+ durationSec = detail.lengthSeconds,
+ albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
+ source = Source.YouTube,
+ albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
+ downloaded = if (dir.isPresent(
+ dir.finalOutputDir(
+ itemName = name,
+ type = folderType,
+ subFolder = subFolder,
+ defaultDir = dir.defaultDir()
+ )
+ )
+ )
+ DownloadStatus.Downloaded
+ else {
+ DownloadStatus.NotDownloaded
+ },
+ outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
+ videoID = searchId
+ )
+ )
+ title = name
+ }
}
}
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt
similarity index 60%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt
index f40d62b6..6e4f4261 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/audioToMp3/AudioToMp3.kt
@@ -1,15 +1,15 @@
-package com.shabinder.common.di.audioToMp3
+package com.shabinder.common.di.providers.requests.audioToMp3
import co.touchlab.kermit.Kermit
import com.shabinder.common.models.AudioQuality
-import io.ktor.client.HttpClient
-import io.ktor.client.request.forms.formData
-import io.ktor.client.request.forms.submitFormWithBinaryData
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import io.ktor.client.request.headers
-import io.ktor.client.statement.HttpStatement
-import io.ktor.http.isSuccess
+import com.shabinder.common.models.SpotiFlyerException
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import io.ktor.client.*
+import io.ktor.client.features.*
+import io.ktor.client.request.*
+import io.ktor.client.request.forms.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
import kotlinx.coroutines.delay
interface AudioToMp3 {
@@ -32,9 +32,10 @@ interface AudioToMp3 {
suspend fun convertToMp3(
URL: String,
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
- ): String? {
- val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
- val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
+ ): SuspendableEvent = SuspendableEvent {
+ // Active Host ex - https://hostveryfast.onlineconverter.com/file/send
+ // Convert Job Request ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
+ var (activeHost,jobLink) = convertRequest(URL, audioQuality).value
// (jobStatus.contains("d")) == COMPLETION
var jobStatus: String
@@ -47,17 +48,23 @@ interface AudioToMp3 {
)
} catch (e: Exception) {
e.printStackTrace()
+ if(e is ClientRequestException && e.response.status.value == 404) {
+ // No Need to Retry, Host/Converter is Busy
+ throw SpotiFlyerException.MP3ConversionFailed(e.message)
+ }
+ // Try Using New Host/Converter
+ convertRequest(URL, audioQuality).value.also {
+ activeHost = it.first
+ jobLink = it.second
+ }
""
}
retryCount--
logger.i("Job Status") { jobStatus }
- if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
- } while (!jobStatus.contains("d", true) && retryCount != 0)
+ if (!jobStatus.contains("d")) delay(600) // Add Delay , to give Server Time to process audio
+ } while (!jobStatus.contains("d", true) && retryCount > 0)
- return if (jobStatus.equals("d", true)) {
- // Return MP3 Download Link
- "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
- } else null
+ "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
}
/*
@@ -66,11 +73,10 @@ interface AudioToMp3 {
* */
private suspend fun convertRequest(
URL: String,
- host: String? = null,
audioQuality: AudioQuality = AudioQuality.KBPS160,
- ): String {
- val activeHost = host ?: getHost()
- val res = client.submitFormWithBinaryData(
+ ): SuspendableEvent,Throwable> = SuspendableEvent {
+ val activeHost by getHost()
+ val convertJob = client.submitFormWithBinaryData(
url = activeHost,
formData = formData {
append("class", "audio")
@@ -87,28 +93,30 @@ interface AudioToMp3 {
header("Referer", "https://www.onlineconverter.com/")
}
}.run {
- logger.d { this }
+ // logger.d { this }
dropLast(3) // last 3 are useless unicode char
}
- val job = client.get(res) {
+ val job = client.get(convertJob) {
headers {
header("Host", "www.onlineconverter.com")
}
}.execute()
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
- return res
+
+ Pair(activeHost,convertJob)
}
// Active Host free to process conversion
// ex - https://hostveryfast.onlineconverter.com/file/send
- private suspend fun getHost(): String {
- return client.get("https://www.onlineconverter.com/get/host") {
+ private suspend fun getHost(): SuspendableEvent = SuspendableEvent {
+ client.get("https://www.onlineconverter.com/get/host") {
headers {
header("Host", "www.onlineconverter.com")
}
- }.also { logger.i("Active Host") { it } }
+ }//.also { logger.i("Active Host") { it } }
}
+
// Extract full Domain from URL
// ex - hostveryfast.onlineconverter.com
private fun String.getHostDomain(): String {
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt
similarity index 90%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt
index 99a232af..373945ae 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/gaana/GaanaRequests.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/gaana/GaanaRequests.kt
@@ -14,23 +14,17 @@
* * along with this program. If not, see .
*/
-package com.shabinder.common.di.gaana
+package com.shabinder.common.di.providers.requests.gaana
-import com.shabinder.common.di.currentPlatform
-import com.shabinder.common.models.AllPlatforms
-import com.shabinder.common.models.corsProxy
+import com.shabinder.common.models.corsApi
import com.shabinder.common.models.gaana.GaanaAlbum
import com.shabinder.common.models.gaana.GaanaArtistDetails
import com.shabinder.common.models.gaana.GaanaArtistTracks
import com.shabinder.common.models.gaana.GaanaPlaylist
import com.shabinder.common.models.gaana.GaanaSong
-import io.ktor.client.HttpClient
-import io.ktor.client.request.get
+import io.ktor.client.*
+import io.ktor.client.request.*
-val corsApi get() = if (currentPlatform is AllPlatforms.Js) {
- corsProxy.url
-} // "https://spotiflyer-cors.azurewebsites.net/" //"https://spotiflyer-cors.herokuapp.com/"//"https://cors.bridged.cc/"
-else ""
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt
similarity index 74%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt
index 3c6ceb7c..b3b59ecf 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnRequests.kt
@@ -1,21 +1,24 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.providers.requests.saavn
import co.touchlab.kermit.Kermit
-import com.shabinder.common.di.audioToMp3.AudioToMp3
-import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.di.globalJson
+import com.shabinder.common.di.providers.requests.audioToMp3.AudioToMp3
+import com.shabinder.common.models.corsApi
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.map
+import com.shabinder.common.models.event.coroutines.success
import com.shabinder.common.models.saavn.SaavnAlbum
import com.shabinder.common.models.saavn.SaavnPlaylist
import com.shabinder.common.models.saavn.SaavnSearchResult
import com.shabinder.common.models.saavn.SaavnSong
+import com.shabinder.common.requireNotNull
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.github.shabinder.utils.getBoolean
import io.github.shabinder.utils.getJsonArray
import io.github.shabinder.utils.getJsonObject
import io.github.shabinder.utils.getString
-import io.ktor.client.HttpClient
-import io.ktor.client.features.ServerResponseException
-import io.ktor.client.request.get
+import io.ktor.client.*
+import io.ktor.client.request.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -24,6 +27,7 @@ import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
+import kotlin.collections.set
interface JioSaavnRequests {
@@ -31,63 +35,64 @@ interface JioSaavnRequests {
val httpClient: HttpClient
val logger: Kermit
- suspend fun findSongDownloadURL(
+ suspend fun findMp3SongDownloadURL(
trackName: String,
trackArtists: List,
- ): String? {
- val songs = searchForSong(trackName)
+ ): SuspendableEvent = searchForSong(trackName).map { songs ->
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
- val m4aLink: String? = bestMatches.keys.firstOrNull()?.let {
- getSongFromID(it).media_url
+
+ val m4aLink: String by getSongFromID(bestMatches.keys.first()).map { song ->
+ song.media_url.requireNotNull()
}
- val mp3Link = m4aLink?.let { audioToMp3.convertToMp3(it) }
- return mp3Link
+
+ val mp3Link by audioToMp3.convertToMp3(m4aLink)
+
+ mp3Link
}
suspend fun searchForSong(
query: String,
includeLyrics: Boolean = false
- ): List {
- /*if (query.startsWith("http") && query.contains("saavn.com")) {
- return listOf(getSong(query))
- }*/
+ ): SuspendableEvent,Throwable> = SuspendableEvent {
val searchURL = search_base_url + query
val results = mutableListOf()
- try {
- (globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach {
- (it as? JsonObject)?.formatData()?.let { jsonObject ->
+
+ (globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject)
+ .getJsonObject("songs")
+ .getJsonArray("data").requireNotNull().forEach {
+ (it as JsonObject).formatData().let { jsonObject ->
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
}
- }
- }catch (e: ServerResponseException) {}
- return results
+ }
+
+ results
}
- suspend fun getLyrics(ID: String): String? {
- return try {
- (Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
- .getString("lyrics")
- }catch (e:Exception) { null }
+ suspend fun getLyrics(ID: String): SuspendableEvent = SuspendableEvent {
+ (Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
+ .getString("lyrics").requireNotNull()
}
suspend fun getSong(
URL: String,
fetchLyrics: Boolean = false
- ): SaavnSong {
+ ): SuspendableEvent = SuspendableEvent {
val id = getSongID(URL)
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
.formatData(fetchLyrics)
- return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
+
+ globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
}
suspend fun getSongFromID(
ID: String,
fetchLyrics: Boolean = false
- ): SaavnSong {
+ ): SuspendableEvent = SuspendableEvent {
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
.formatData(fetchLyrics)
- return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
+
+ globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
}
private suspend fun getSongID(
@@ -104,24 +109,19 @@ interface JioSaavnRequests {
suspend fun getPlaylist(
URL: String,
includeLyrics: Boolean = false
- ): SaavnPlaylist? {
- return try {
- globalJson.decodeFromJsonElement(
- SaavnPlaylist.serializer(),
- (globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject)
- .formatData(includeLyrics)
- )
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+ ): SuspendableEvent = SuspendableEvent {
+ globalJson.decodeFromJsonElement(
+ SaavnPlaylist.serializer(),
+ (globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject)
+ .formatData(includeLyrics)
+ )
}
private suspend fun getPlaylistID(
URL: String
- ): String {
+ ): SuspendableEvent = SuspendableEvent {
val res = httpClient.get(URL)
- return try {
+ try {
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
} catch (e: IndexOutOfBoundsException) {
res.split("\"page_id\",\"")[1].split("\",\"")[0]
@@ -131,24 +131,19 @@ interface JioSaavnRequests {
suspend fun getAlbum(
URL: String,
includeLyrics: Boolean = false
- ): SaavnAlbum? {
- return try {
- globalJson.decodeFromJsonElement(
- SaavnAlbum.serializer(),
- (globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL))) as JsonObject)
- .formatData(includeLyrics)
- )
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+ ): SuspendableEvent = SuspendableEvent {
+ globalJson.decodeFromJsonElement(
+ SaavnAlbum.serializer(),
+ (globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject)
+ .formatData(includeLyrics)
+ )
}
private suspend fun getAlbumID(
URL: String
- ): String {
+ ): SuspendableEvent = SuspendableEvent {
val res = httpClient.get(URL)
- return try {
+ try {
res.split("\"album_id\":\"")[1].split('"')[0]
} catch (e: IndexOutOfBoundsException) {
res.split("\"page_id\",\"")[1].split("\",\"")[0]
@@ -214,8 +209,10 @@ interface JioSaavnRequests {
// Fetch Lyrics if Requested
// Lyrics is HTML Based
if (includeLyrics) {
- if (getBoolean("has_lyrics") == true) {
- put("lyrics", getString("id")?.let { getLyrics(it) })
+ if (getBoolean("has_lyrics") == true && containsKey("id")) {
+ getLyrics(getString("id").requireNotNull()).success {
+ put("lyrics", it)
+ }
} else {
put("lyrics", "")
}
@@ -237,8 +234,8 @@ interface JioSaavnRequests {
for (result in tracks) {
var hasCommonWord = false
- val resultName = result.title.toLowerCase().replace("/", " ")
- val trackNameWords = trackName.toLowerCase().split(" ")
+ val resultName = result.title.lowercase().replace("/", " ")
+ val trackNameWords = trackName.lowercase().split(" ")
for (nameWord in trackNameWords) {
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
@@ -258,11 +255,11 @@ interface JioSaavnRequests {
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
- result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
+ result.more_info?.primary_artists?.lowercase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
- if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
+ if (FuzzySearch.partialRatio(artist.lowercase(), artistListString) > 85)
artistMatchNumber++
}
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt
similarity index 70%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt
index d9e38f2d..4ff3e563 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/saavn/JioSaavnUtils.kt
@@ -1,4 +1,6 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.providers.requests.saavn
+
+import com.shabinder.common.di.utils.unescape
expect suspend fun decryptURL(url: String): String
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt
similarity index 60%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt
index 363eaf17..4bb67403 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyAuth.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyAuth.kt
@@ -14,30 +14,29 @@
* * along with this program. If not, see .
*/
-package com.shabinder.common.di.spotify
+package com.shabinder.common.di.providers.requests.spotify
import com.shabinder.common.di.globalJson
+import com.shabinder.common.models.SpotiFlyerException
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.methods
import com.shabinder.common.models.spotify.TokenData
-import io.ktor.client.HttpClient
-import io.ktor.client.features.auth.Auth
-import io.ktor.client.features.auth.providers.basic
-import io.ktor.client.features.json.JsonFeature
-import io.ktor.client.features.json.serializer.KotlinxSerializer
-import io.ktor.client.request.forms.FormDataContent
-import io.ktor.client.request.post
-import io.ktor.http.Parameters
+import io.ktor.client.*
+import io.ktor.client.features.auth.*
+import io.ktor.client.features.auth.providers.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.json.serializer.*
+import io.ktor.client.request.*
+import io.ktor.client.request.forms.*
+import io.ktor.http.*
import kotlin.native.concurrent.SharedImmutable
-suspend fun authenticateSpotify(): TokenData? {
- return try {
- if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
+suspend fun authenticateSpotify(): SuspendableEvent = SuspendableEvent {
+ if (methods.value.isInternetAvailable) {
+ spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
- } else null
- } catch (e: Exception) {
- e.printStackTrace()
- null
- }
+ }
+ } else throw SpotiFlyerException.NoInternetException()
}
@SharedImmutable
@@ -48,9 +47,10 @@ private val spotifyAuthClient by lazy {
install(Auth) {
basic {
- sendWithoutRequest = true
- username = clientId
- password = clientSecret
+ sendWithoutRequest { true }
+ credentials {
+ BasicAuthCredentials(clientId, clientSecret)
+ }
}
}
install(JsonFeature) {
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt
similarity index 86%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt
index dba7784e..7e31a4f6 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/spotify/SpotifyRequests.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/spotify/SpotifyRequests.kt
@@ -14,16 +14,18 @@
* * along with this program. If not, see .
*/
-package com.shabinder.common.di.spotify
+package com.shabinder.common.di.providers.requests.spotify
-import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.NativeAtomicReference
+import com.shabinder.common.models.corsApi
import com.shabinder.common.models.spotify.Album
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
import com.shabinder.common.models.spotify.Playlist
import com.shabinder.common.models.spotify.Track
-import io.ktor.client.HttpClient
-import io.ktor.client.request.get
+import io.github.shabinder.TargetPlatforms
+import io.github.shabinder.activePlatform
+import io.ktor.client.*
+import io.ktor.client.request.*
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
@@ -32,7 +34,7 @@ interface SpotifyRequests {
val httpClientRef: NativeAtomicReference
val httpClient: HttpClient get() = httpClientRef.value
- suspend fun authenticateSpotifyClient(override: Boolean = false)
+ suspend fun authenticateSpotifyClient(override: Boolean = activePlatform is TargetPlatforms.Js)
suspend fun getPlaylist(playlistID: String): Playlist {
return httpClient.get("$BASE_URL/playlists/$playlistID")
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt
similarity index 59%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt
index 4a1613a5..3e7feff2 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/youtubeMp3/Yt1sMp3.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/requests/youtubeMp3/Yt1sMp3.kt
@@ -14,14 +14,18 @@
* * along with this program. If not, see .
*/
-package com.shabinder.common.di.youtubeMp3
+package com.shabinder.common.di.providers.requests.youtubeMp3
import co.touchlab.kermit.Kermit
-import com.shabinder.common.di.gaana.corsApi
-import io.ktor.client.HttpClient
-import io.ktor.client.request.forms.FormDataContent
-import io.ktor.client.request.post
-import io.ktor.http.Parameters
+import com.shabinder.common.models.corsApi
+import com.shabinder.common.models.event.coroutines.SuspendableEvent
+import com.shabinder.common.models.event.coroutines.flatMap
+import com.shabinder.common.models.event.coroutines.map
+import com.shabinder.common.requireNotNull
+import io.ktor.client.*
+import io.ktor.client.request.*
+import io.ktor.client.request.forms.*
+import io.ktor.http.*
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
@@ -33,18 +37,23 @@ interface Yt1sMp3 {
val httpClient: HttpClient
val logger: Kermit
+
/*
* Downloadable Mp3 Link for YT videoID.
* */
- suspend fun getLinkFromYt1sMp3(videoID: String): String? =
- getConvertedMp3Link(videoID, getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "")
+ suspend fun getLinkFromYt1sMp3(videoID: String): SuspendableEvent = getKey(videoID).flatMap { key ->
+ getConvertedMp3Link(videoID, key).map {
+ it["dlink"].requireNotNull()
+ .jsonPrimitive.content.replace("\"", "")
+ }
+ }
/*
* POST:https://yt1s.com/api/ajaxSearch/index
* Body Form= q:yt video link ,vt:format=mp3
* */
- private suspend fun getKey(videoID: String): String {
- val response: JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
+ private suspend fun getKey(videoID: String): SuspendableEvent = SuspendableEvent {
+ val response: JsonObject = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
body = FormDataContent(
Parameters.build {
append("q", "https://www.youtube.com/watch?v=$videoID")
@@ -52,11 +61,12 @@ interface Yt1sMp3 {
}
)
}
- return response?.get("kc")?.jsonPrimitive.toString()
+
+ response["kc"].requireNotNull().jsonPrimitive.content
}
- private suspend fun getConvertedMp3Link(videoID: String, key: String): JsonObject? {
- return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
+ private suspend fun getConvertedMp3Link(videoID: String, key: String): SuspendableEvent = SuspendableEvent {
+ httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
body = FormDataContent(
Parameters.build {
append("vid", videoID)
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt
similarity index 95%
rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt
rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt
index eb845e0e..3e06f0b5 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/JsonUtils.kt
@@ -1,4 +1,4 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.utils
/*
* JSON UTILS
@@ -6,7 +6,7 @@ package com.shabinder.common.di.saavn
fun String.escape(): String {
val output = StringBuilder()
for (element in this) {
- val chx = element.toInt()
+ val chx = element.code
if (chx != 0) {
when (element) {
'\n' -> {
@@ -76,7 +76,7 @@ fun String.unescape(): String {
/*if (!x.isLetterOrDigit()) {
throw RuntimeException("Bad character in unicode escape.")
}*/
- hex.append(x.toLowerCase())
+ hex.append(x.lowercaseChar())
}
i += 4 // consume those four digits.
val code = hex.toString().toInt(16)
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 13aa686a..7fa50889 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
@@ -22,7 +22,7 @@ package com.shabinder.common.di.utils
// 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.*
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
@@ -96,7 +96,7 @@ class ParallelExecutor(
return
var change = expectedCount - actualCount
- while (change > 0 && killQueue.poll() != null)
+ while (change > 0 && killQueue.tryReceive().getOrNull() != null)
change -= 1
if (change > 0)
@@ -104,7 +104,7 @@ class ParallelExecutor(
repeat(change) { launchProcessor() }
}
else
- repeat(-change) { killQueue.offer(Unit) }
+ repeat(-change) { killQueue.trySend(Unit).isSuccess }
}
private class Operation(
diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
index 6bf00c69..4e9cda1e 100644
--- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
+++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
@@ -17,9 +17,9 @@
package com.shabinder.common.di
import com.shabinder.common.di.utils.ParallelExecutor
-import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@@ -34,9 +34,6 @@ val DownloadScope = ParallelExecutor(Dispatchers.IO)
// IO-Dispatcher
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.IO
-// Current Platform Info
-actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
-
actual suspend fun downloadTracks(
list: List,
fetcher: FetchPlatformQueryResult,
@@ -44,41 +41,43 @@ actual suspend fun downloadTracks(
) {
list.forEach { trackDetails ->
DownloadScope.execute { // Send Download to Pool.
- val url = fetcher.findMp3DownloadLink(trackDetails)
- if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
- downloadFile(url).collect {
- when (it) {
- is DownloadResult.Error -> {
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
- )
- }
- is DownloadResult.Progress -> {
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
- )
- }
- is DownloadResult.Success -> { // Todo clear map
- dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
- )
+ fetcher.findMp3DownloadLink(trackDetails).fold(
+ success = { url ->
+ downloadFile(url).collect {
+ when (it) {
+ is DownloadResult.Error -> {
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause))) }
+ )
+ }
+ is DownloadResult.Progress -> {
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
+ )
+ }
+ is DownloadResult.Success -> { // Todo clear map
+ dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
+ )
+ }
}
}
+ },
+ failure = { error ->
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed(error)) }
+ )
}
- } else {
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
- )
- }
+ )
}
}
}
diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt
index ae393df9..efff1f9a 100644
--- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt
+++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt
@@ -20,8 +20,8 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit
import com.mpatric.mp3agic.Mp3File
-import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinx.coroutines.Dispatchers
@@ -40,7 +40,7 @@ import javax.imageio.ImageIO
actual class Dir actual constructor(
private val logger: Kermit,
- settingsPref: Settings,
+ private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@@ -55,7 +55,7 @@ actual class Dir actual constructor(
private val defaultBaseDir = System.getProperty("user.home")
- actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
+ actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) + fileSeparator() +
"SpotiFlyer" + fileSeparator()
actual fun isPresent(path: String): Boolean = File(path).exists()
@@ -199,7 +199,6 @@ actual class Dir actual constructor(
}
actual val db: Database? = spotiFlyerDatabase.instance
- actual val settings: Settings = settingsPref
}
fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(
diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
similarity index 86%
rename from common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
rename to common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
index 80153b91..e77b3045 100644
--- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
+++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
@@ -1,7 +1,6 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.providers.requests.saavn
-import io.ktor.util.InternalAPI
-import io.ktor.util.decodeBase64Bytes
+import io.ktor.util.*
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey
diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt
index 10dae478..5ea6efaa 100644
--- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt
+++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt
@@ -1,8 +1,8 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
-import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import kotlinx.coroutines.GlobalScope
@@ -24,7 +24,7 @@ import platform.UIKit.UIImageJPEGRepresentation
actual class Dir actual constructor(
val logger: Kermit,
- settingsPref: Settings,
+ private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
@@ -35,7 +35,7 @@ actual class Dir actual constructor(
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!.path!!
// TODO Error Handling
- actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
+ actual fun defaultDir(): String = (preferenceManager.downloadDir ?: defaultBaseDir) +
fileSeparator() + "SpotiFlyer" + fileSeparator()
private val defaultDirURL: NSURL by lazy {
@@ -176,6 +176,5 @@ actual class Dir actual constructor(
// TODO
}
- actual val settings: Settings = settingsPref
actual val db: Database? = spotiFlyerDatabase.instance
}
diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/providers.requests/saavn/decryptURL.kt
similarity index 100%
rename from common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt
rename to common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/providers.requests/saavn/decryptURL.kt
diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
index 7a01defa..0682cc22 100644
--- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
+++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
@@ -16,9 +16,9 @@
package com.shabinder.common.di
-import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
+import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@@ -34,9 +34,6 @@ val allTracksStatus: HashMap = hashMapOf()
// IO-Dispatcher
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
-// Current Platform Info
-actual val currentPlatform: AllPlatforms = AllPlatforms.Js
-
actual suspend fun downloadTracks(
list: List,
fetcher: FetchPlatformQueryResult,
@@ -45,29 +42,31 @@ actual suspend fun downloadTracks(
list.forEach { track ->
withContext(dispatcherIO) {
allTracksStatus[track.title] = DownloadStatus.Queued
- val url = fetcher.findMp3DownloadLink(track)
- if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
- downloadFile(url).collect {
- when (it) {
- is DownloadResult.Success -> {
- println("Download Completed")
- dir.saveFileWithMetadata(it.byteArray, track) {}
- }
- is DownloadResult.Error -> {
- allTracksStatus[track.title] = DownloadStatus.Failed
- println("Download Error: ${track.title}")
- }
- is DownloadResult.Progress -> {
- allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
- println("Download Progress: ${it.progress} : ${track.title}")
+ fetcher.findMp3DownloadLink(track).fold(
+ success = { url ->
+ downloadFile(url).collect {
+ when (it) {
+ is DownloadResult.Success -> {
+ println("Download Completed")
+ dir.saveFileWithMetadata(it.byteArray, track) {}
+ }
+ is DownloadResult.Error -> {
+ allTracksStatus[track.title] = DownloadStatus.Failed(it.cause ?: SpotiFlyerException.UnknownReason(it.cause))
+ println("Download Error: ${track.title}")
+ }
+ is DownloadResult.Progress -> {
+ allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
+ println("Download Progress: ${it.progress} : ${track.title}")
+ }
}
+ DownloadProgressFlow.emit(allTracksStatus)
}
+ },
+ failure = { error ->
+ allTracksStatus[track.title] = DownloadStatus.Failed(error)
DownloadProgressFlow.emit(allTracksStatus)
}
- } else {
- allTracksStatus[track.title] = DownloadStatus.Failed
- DownloadProgressFlow.emit(allTracksStatus)
- }
+ )
}
}
}
diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt
index c909d10e..cc18e5d2 100644
--- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt
+++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt
@@ -17,13 +17,13 @@
package com.shabinder.common.di
import co.touchlab.kermit.Kermit
-import com.russhwolf.settings.Settings
import com.shabinder.common.database.SpotiFlyerDatabase
-import com.shabinder.common.di.gaana.corsApi
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
+import com.shabinder.common.models.corsApi
import com.shabinder.database.Database
import kotlinext.js.Object
import kotlinext.js.js
@@ -34,7 +34,7 @@ import org.w3c.dom.ImageBitmap
actual class Dir actual constructor(
private val logger: Kermit,
- settingsPref: Settings,
+ private val preferenceManager: PreferenceManager,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
/*init {
@@ -116,7 +116,6 @@ actual class Dir actual constructor(
private suspend fun freshImage(url: String): ImageBitmap? = null
actual val db: Database? = spotiFlyerDatabase.instance
- actual val settings: Settings = settingsPref
}
fun ByteArray.toArrayBuffer(): ArrayBuffer {
diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
similarity index 60%
rename from common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
rename to common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
index e5264bbb..540525fe 100644
--- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt
+++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/providers/requests/saavn/decryptURL.kt
@@ -1,4 +1,4 @@
-package com.shabinder.common.di.saavn
+package com.shabinder.common.di.providers.requests.saavn
actual suspend fun decryptURL(url: String): String {
TODO("Not yet implemented")
diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt
index a93b7908..037eb808 100644
--- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt
+++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt
@@ -22,6 +22,7 @@ import com.arkivanov.mvikotlin.core.store.StoreFactory
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.Picture
+import com.shabinder.common.di.preference.PreferenceManager
import com.shabinder.common.list.integration.SpotiFlyerListImpl
import com.shabinder.common.models.Consumer
import com.shabinder.common.models.DownloadStatus
@@ -61,12 +62,13 @@ interface SpotiFlyerList {
/*
* Snooze Donation Dialog
* */
- fun snoozeDonationDialog()
+ fun dismissDonationDialogSetOffset()
interface Dependencies {
val storeFactory: StoreFactory
val fetchQuery: FetchPlatformQueryResult
val dir: Dir
+ val preferenceManager: PreferenceManager
val link: String
val listOutput: Consumer