Code Cleanup and Formatting and i18n4k dependency

This commit is contained in:
shabinder 2021-07-14 02:15:09 +05:30
parent c591842fb4
commit 116530cc3c
80 changed files with 315 additions and 328 deletions

View File

@ -23,6 +23,7 @@ plugins {
kotlin("android")
id("kotlin-parcelize")
id("org.jetbrains.compose")
id("ktlint-setup")
}
group = "com.shabinder"
@ -39,7 +40,7 @@ repositories {
android {
val props = gradleLocalProperties(rootDir)
if(props.containsKey("storeFileDir")) {
if (props.containsKey("storeFileDir")) {
signingConfigs {
create("release") {
storeFile = file(props.getProperty("storeFileDir"))
@ -65,7 +66,7 @@ android {
getByName("release") {
isMinifyEnabled = true
// isShrinkResources = true
if(props.containsKey("storeFileDir")){
if (props.containsKey("storeFileDir")) {
signingConfig = signingConfigs.getByName("release")
}
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
@ -134,7 +135,7 @@ dependencies {
}
implementation(Extras.kermit)
//implementation("com.jakewharton.timber:timber:4.7.1")
// implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:${Versions.mokoParcelize}")
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
implementation("com.google.accompanist:accompanist-insets:0.12.0")

View File

@ -34,7 +34,7 @@ import org.matomo.sdk.Matomo
import org.matomo.sdk.Tracker
import org.matomo.sdk.TrackerBuilder
class App: Application(), KoinComponent {
class App : Application(), KoinComponent {
companion object {
const val SIGNATURE_HEX = "53304f6d75736a2f30484230334c454b714753525763724259444d3d0a"
@ -42,7 +42,8 @@ class App: Application(), KoinComponent {
val tracker: Tracker by lazy {
TrackerBuilder.createDefault(
"https://matomo.spotiflyer.ml/matomo.php", 1)
"https://matomo.spotiflyer.ml/matomo.php", 1
)
.build(Matomo.getInstance(this)).apply {
if (BuildConfig.DEBUG) {
/*Timber.plant(DebugTree())

View File

@ -98,7 +98,6 @@ import org.koin.android.ext.android.inject
import org.matomo.sdk.extra.TrackHelper
import java.io.File
@ExperimentalAnimationApi
class MainActivity : ComponentActivity() {
@ -167,7 +166,7 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(view) {
permissionGranted.value = checkPermissions()
if(preferenceManager.isFirstLaunch) {
if (preferenceManager.isFirstLaunch) {
delay(2500)
// Ask For Analytics Permission on first Dialog
askForAnalyticsPermission = true
@ -187,8 +186,8 @@ class MainActivity : ComponentActivity() {
* 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(preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
if (isGithubRelease) { checkIfLatestVersion() }
if (preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
// Download/App Install Event for F-Droid builds
TrackHelper.track().download().with(tracker)
}
@ -248,7 +247,6 @@ class MainActivity : ComponentActivity() {
}
/*END: Foreground Service Handlers*/
@Composable
private fun isInternetAvailableState(): State<Boolean?> {
return internetAvailability.observeAsState()
@ -258,7 +256,7 @@ class MainActivity : ComponentActivity() {
android.widget.Toast.makeText(
applicationContext,
string,
if(long) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT
if (long) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT
).show()
}
@ -270,23 +268,24 @@ class MainActivity : ComponentActivity() {
private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
SpotiFlyerRoot(
componentContext,
dependencies = object : SpotiFlyerRoot.Dependencies{
dependencies = object : SpotiFlyerRoot.Dependencies {
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
override val database = this@MainActivity.dir.db
override val fetchQuery = this@MainActivity.fetcher
override val dir: Dir = this@MainActivity.dir
override val preferenceManager = this@MainActivity.preferenceManager
override val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
override val actions = object: Actions {
override val actions = object : Actions {
override val platformActions = object : PlatformActions {
override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator
override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey,
override val sharedPreferences = applicationContext.getSharedPreferences(
SharedPreferencesKey,
MODE_PRIVATE
)
override fun addToLibrary(path: String) {
MediaScannerConnection.scanFile (
MediaScannerConnection.scanFile(
applicationContext,
listOf(path).toTypedArray(), null, null
)
@ -298,14 +297,14 @@ class MainActivity : ComponentActivity() {
}
}
override fun showPopUpMessage(string: String, long: Boolean) = this@MainActivity.showPopUpMessage(string,long)
override fun showPopUpMessage(string: String, long: Boolean) = this@MainActivity.showPopUpMessage(string, long)
override fun setDownloadDirectoryAction(callBack: (String) -> Unit) = setUpOnPrefClickListener(callBack)
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")
openPlatform("", platformLink = "https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button")
}
override fun shareApp() {
@ -342,25 +341,25 @@ class MainActivity : ComponentActivity() {
}
}
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
override fun writeMp3Tags(trackDetails: TrackDetails) { /*IMPLEMENTED*/ }
override val isInternetAvailable get() = internetAvailability.value ?: true
override val isInternetAvailable get() = internetAvailability.value ?: true
}
/*
* Analytics Will Only Be Sent if User Granted us the Permission
* */
override val analytics = object: Analytics {
override val analytics = object : Analytics {
override fun appLaunchEvent() {
if(preferenceManager.isAnalyticsEnabled){
if (preferenceManager.isAnalyticsEnabled) {
TrackHelper.track()
.event("events","App_Launch")
.event("events", "App_Launch")
.name("App Launch").with(tracker)
}
}
override fun homeScreenVisit() {
if(preferenceManager.isAnalyticsEnabled){
if (preferenceManager.isAnalyticsEnabled) {
// HomeScreen Visit Event
TrackHelper.track().screen("/main_activity/home_screen")
.title("HomeScreen").with(tracker)
@ -368,7 +367,7 @@ class MainActivity : ComponentActivity() {
}
override fun listScreenVisit() {
if(preferenceManager.isAnalyticsEnabled){
if (preferenceManager.isAnalyticsEnabled) {
// ListScreen Visit Event
TrackHelper.track().screen("/main_activity/list_screen")
.title("ListScreen").with(tracker)
@ -400,15 +399,17 @@ class MainActivity : ComponentActivity() {
}
@Suppress("DEPRECATION")
private fun setUpOnPrefClickListener(callBack : (String) -> Unit) {
private fun setUpOnPrefClickListener(callBack: (String) -> Unit) {
// 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)
})
.setTheme(
StorageChooser.Theme(applicationContext).apply {
scheme = applicationContext.resources.getIntArray(R.array.default_dark)
}
)
.setDialogTitle(Strings.setDownloadDirectory())
.allowCustomPath(true)
.setType(StorageChooser.DIRECTORY_CHOOSER)
@ -423,7 +424,7 @@ class MainActivity : ComponentActivity() {
preferenceManager.setDownloadDirectory(path)
callBack(path)
showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}"))
}else{
} else {
showPopUpMessage(Strings.noWriteAccess("\n$path "))
}
}
@ -445,7 +446,7 @@ class MainActivity : ComponentActivity() {
// Ignoring battery optimization
permissionGranted.value = true
} else {
disableDozeMode(disableDozeCode)//Again Ask For Permission!!
disableDozeMode(disableDozeCode) // Again Ask For Permission!!
}
}
}
@ -463,12 +464,12 @@ class MainActivity : ComponentActivity() {
val filterLinkRegex = """http.+\w""".toRegex()
val string = it.replace("\n".toRegex(), " ")
val link = filterLinkRegex.find(string)?.value.toString()
Log.i("Intent",link)
Log.i("Intent", link)
lifecycleScope.launch {
while(!this@MainActivity::root.isInitialized){
while (!this@MainActivity::root.isInitialized) {
delay(100)
}
if(methods.value.isInternetAvailable)callBacks.searchLink(link)
if (methods.value.isInternetAvailable)callBacks.searchLink(link)
}
}
}

View File

@ -18,4 +18,4 @@ package com.shabinder.spotiflyer.di
import org.koin.dsl.module
fun appModule(enableLogging:Boolean = false) = module {}
fun appModule(enableLogging: Boolean = false) = module {}

View File

@ -57,7 +57,7 @@ import java.io.File
class ForegroundService : LifecycleService() {
private var downloadService: AutoClear<ParallelExecutor> = autoClear { ParallelExecutor(Dispatchers.IO) }
val trackStatusFlowMap by autoClear { TrackStatusFlowMap(MutableSharedFlow(replay = 1),lifecycleScope) }
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()
@ -133,18 +133,18 @@ class ForegroundService : LifecycleService() {
updateNotification()
}
trackList.forEach {
trackStatusFlowMap[it.title] = DownloadStatus.Queued
for (track in trackList) {
trackStatusFlowMap[track.title] = DownloadStatus.Queued
lifecycleScope.launch {
downloadService.value.execute {
fetcher.findMp3DownloadLink(it).fold(
fetcher.findMp3DownloadLink(track).fold(
success = { url ->
enqueueDownload(url, it)
enqueueDownload(url, track)
},
failure = { error ->
failed++
updateNotification()
trackStatusFlowMap[it.title] = DownloadStatus.Failed(error)
trackStatusFlowMap[track.title] = DownloadStatus.Failed(error)
}
)
}
@ -238,7 +238,7 @@ class ForegroundService : LifecycleService() {
lifecycleScope.launch {
logger.d(TAG) { "Killing Self" }
messageList = messageList.getEmpty().apply {
set(index = 0, Message(Strings.cleaningAndExiting(),DownloadStatus.NotDownloaded))
set(index = 0, Message(Strings.cleaningAndExiting(), DownloadStatus.NotDownloaded))
}
downloadService.getOrNull()?.close()
downloadService.reset()
@ -260,7 +260,7 @@ class ForegroundService : LifecycleService() {
setSmallIcon(R.drawable.ic_download_arrow)
setContentTitle("${Strings.total()}: $total ${Strings.completed()}:$converted ${Strings.failed()}:$failed")
setSilent(true)
setProgress(total,failed+converted,false)
setProgress(total, failed + converted, false)
setStyle(
NotificationCompat.InboxStyle().run {
addLine(messageList[messageList.size - 1].asString())

View File

@ -7,7 +7,7 @@ typealias Message = Pair<String, DownloadStatus>
val Message.title: String get() = first
val Message.downloadStatus: DownloadStatus get() = second
val Message.downloadStatus: DownloadStatus get() = second
val Message.progress: String get() = when (downloadStatus) {
is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%"
@ -18,12 +18,12 @@ val Message.progress: String get() = when (downloadStatus) {
is DownloadStatus.NotDownloaded -> ""
}
val emptyMessage = Message("",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){
val statusString = when (downloadStatus) {
is DownloadStatus.Downloading -> Strings.downloading()
is DownloadStatus.Converting -> Strings.processing()
else -> ""

View File

@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
class TrackStatusFlowMap(
val statusFlow: MutableSharedFlow<HashMap<String,DownloadStatus>>,
val statusFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>,
private val scope: CoroutineScope
): HashMap<String,DownloadStatus>() {
) : HashMap<String, DownloadStatus>() {
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
val res = super.put(key, value)
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }

View File

@ -8,7 +8,7 @@ import java.io.File
**/
fun cleanFiles(dir: File) {
try {
Log.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) {
@ -16,7 +16,7 @@ fun cleanFiles(dir: File) {
cleanFiles(file)
} else if (file.isFile) {
if (file.path.toString().substringAfterLast(".") != "mp3") {
Log.d("Files Cleaning","Cleaning ${file.path}")
Log.d("Files Cleaning", "Cleaning ${file.path}")
file.delete()
}
}
@ -24,4 +24,3 @@ fun cleanFiles(dir: File) {
}
} catch (e: Exception) { e.printStackTrace() }
}

View File

@ -34,8 +34,8 @@ import com.shabinder.common.uikit.configurations.colorPrimary
@ExperimentalAnimationApi
@Composable
fun AnalyticsDialog(
isVisible:Boolean,
enableAnalytics: ()->Unit,
isVisible: Boolean,
enableAnalytics: () -> Unit,
dismissDialog: () -> Unit,
) {
// Analytics Permission Dialog
@ -43,10 +43,10 @@ fun AnalyticsDialog(
AlertDialog(
onDismissRequest = dismissDialog,
title = {
Row(verticalAlignment = Alignment.CenterVertically,horizontalArrangement = Arrangement.SpaceEvenly) {
Icon(Icons.Rounded.Insights,Strings.analytics(), Modifier.size(42.dp))
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly) {
Icon(Icons.Rounded.Insights, Strings.analytics(), Modifier.size(42.dp))
Spacer(Modifier.padding(horizontal = 8.dp))
Text(Strings.grantAnalytics(),style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center)
Text(Strings.grantAnalytics(), style = SpotiFlyerTypography.h5, textAlign = TextAlign.Center)
}
},
backgroundColor = Color.DarkGray,
@ -60,7 +60,7 @@ fun AnalyticsDialog(
shape = SpotiFlyerShapes.medium,
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF303030))
) {
Text(Strings.no(),color = colorPrimary,fontSize = 18.sp,textAlign = TextAlign.Center)
Text(Strings.no(), color = colorPrimary, fontSize = 18.sp, textAlign = TextAlign.Center)
}
Spacer(Modifier.padding(vertical = 4.dp))
TextButton(
@ -73,14 +73,14 @@ fun AnalyticsDialog(
.padding(horizontal = 8.dp),
shape = SpotiFlyerShapes.medium
) {
Text(Strings.yes(),color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
Text(Strings.yes(), color = Color.Black, fontSize = 18.sp, textAlign = TextAlign.Center)
}
}
},
text = {
Text(Strings.analyticsDescription(),style = SpotiFlyerTypography.body2,textAlign = TextAlign.Center)
Text(Strings.analyticsDescription(), style = SpotiFlyerTypography.body2, textAlign = TextAlign.Center)
},
properties = DialogProperties(dismissOnBackPress = false,dismissOnClickOutside = false)
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
)
}
}

View File

@ -53,10 +53,10 @@ import kotlinx.coroutines.delay
@Composable
fun NetworkDialog(
networkAvailability: State<Boolean?>
){
) {
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit){
LaunchedEffect(Unit) {
delay(2600)
visible = true
}
@ -75,21 +75,30 @@ fun NetworkDialog(
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
}
*/},
title = { Text(Strings.noInternetConnection(),
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center) },
*/
},
title = {
Text(
Strings.noInternetConnection(),
style = SpotiFlyerTypography.h5,
textAlign = TextAlign.Center
)
},
backgroundColor = Color.DarkGray,
text = {
Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center){
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Spacer(modifier = Modifier.padding(8.dp))
Row(verticalAlignment = Alignment.CenterVertically,
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
) {
Image(Icons.Rounded.CloudOff,
Strings.noInternetConnection(),Modifier.size(42.dp),colorFilter = ColorFilter.tint(
colorOffWhite
))
Image(
Icons.Rounded.CloudOff,
Strings.noInternetConnection(), Modifier.size(42.dp),
colorFilter = ColorFilter.tint(
colorOffWhite
)
)
Spacer(modifier = Modifier.padding(start = 16.dp))
Text(
text = Strings.checkInternetConnection(),
@ -97,8 +106,8 @@ fun NetworkDialog(
)
}
}
}
,shape = SpotiFlyerShapes.medium
},
shape = SpotiFlyerShapes.medium
)
}
}

View File

@ -38,9 +38,9 @@ import kotlinx.coroutines.delay
@Composable
fun PermissionDialog(
permissionGranted: Boolean,
requestStoragePermission:() -> Unit,
disableDozeMode:() -> Unit,
){
requestStoragePermission: () -> Unit,
disableDozeMode: () -> Unit,
) {
var askForPermission by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(2600)
@ -61,18 +61,20 @@ fun PermissionDialog(
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
.padding(horizontal = 8.dp),
){
Text(Strings.grantPermissions(),color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
) {
Text(Strings.grantPermissions(), color = Color.Black, fontSize = 18.sp, textAlign = TextAlign.Center)
}
},title = { Text(Strings.requiredPermissions(),style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center) },
},
title = { Text(Strings.requiredPermissions(), style = SpotiFlyerTypography.h5, textAlign = TextAlign.Center) },
backgroundColor = Color.DarkGray,
text = {
Column{
Column {
Spacer(modifier = Modifier.padding(8.dp))
Row(verticalAlignment = Alignment.CenterVertically,
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
) {
Icon(Icons.Rounded.SdStorage,Strings.storagePermission())
Icon(Icons.Rounded.SdStorage, Strings.storagePermission())
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
@ -89,7 +91,7 @@ fun PermissionDialog(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.SystemSecurityUpdate,Strings.backgroundRunning())
Icon(Icons.Rounded.SystemSecurityUpdate, Strings.backgroundRunning())
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import com.shabinder.spotiflyer.App
import java.security.MessageDigest
@ -22,14 +21,14 @@ fun checkAppSignature(context: Context): Boolean {
// Log.d("REMOVE_ME", "Include this string as a value for SIGNATURE:$currentSignature")
// Log.d("REMOVE_ME HEX", "Include this string as a value for SIGNATURE Hex:${currentSignature.toByteArray().toHEX()}")
//compare signatures
// compare signatures
if (App.SIGNATURE_HEX == currentSignature.toByteArray().toHEX()) {
return true
}
}
} catch (e: Exception) {
e.printStackTrace()
//assumes an issue in checking signature., but we let the caller decide on what to do.
// assumes an issue in checking signature., but we let the caller decide on what to do.
}
return false
}

View File

@ -32,9 +32,9 @@ import com.github.javiersantos.appupdater.enums.Display
import com.github.javiersantos.appupdater.enums.UpdateFrom
fun Activity.checkIfLatestVersion() {
AppUpdater(this,0).run {
AppUpdater(this, 0).run {
setDisplay(Display.NOTIFICATION)
showAppUpdated(false)//true:Show App is Updated Dialog
showAppUpdated(false) // true:Show App is Updated Dialog
setUpdateFrom(UpdateFrom.XML)
setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/Compose/app/src/main/res/xml/app_update.xml")
setCancelable(false)
@ -42,24 +42,26 @@ fun Activity.checkIfLatestVersion() {
}
}
fun Activity.checkPermissions():Boolean = ContextCompat
.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
&&
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
ContextCompat.checkSelfPermission(this,
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED
} else true
fun Activity.checkPermissions(): Boolean = ContextCompat
.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED &&
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ContextCompat.checkSelfPermission(
this,
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
) == PackageManager.PERMISSION_GRANTED
} else true
@SuppressLint("BatteryLife", "ObsoleteSdkInt")
fun Activity.disableDozeMode(requestCode:Int) {
fun Activity.disableDozeMode(requestCode: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val pm =
this.getSystemService(Context.POWER_SERVICE) as PowerManager
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
if (!isIgnoringBatteryOptimizations) {
val intent = Intent().apply{
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:$packageName")
}

View File

@ -2,7 +2,6 @@ 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
@ -34,7 +33,7 @@ class AutoClear<T : Any?>(
fun getOrNull(): T? = _value
private val observer: LifecycleAutoInitializer<T?> by lazy {
when(trigger) {
when (trigger) {
TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer)
TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer)
TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer)

View File

@ -2,6 +2,6 @@ package com.shabinder.spotiflyer.utils.autoclear
import androidx.lifecycle.DefaultLifecycleObserver
interface LifecycleAutoInitializer<T>: DefaultLifecycleObserver {
interface LifecycleAutoInitializer<T> : DefaultLifecycleObserver {
var value: T?
}

View File

@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleCreateAndDestroyObserver<T: Any?>(
class LifecycleCreateAndDestroyObserver<T : Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {

View File

@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleResumeAndPauseObserver<T: Any?>(
class LifecycleResumeAndPauseObserver<T : Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {

View File

@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
import androidx.lifecycle.LifecycleOwner
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
class LifecycleStartAndStopObserver<T: Any?>(
class LifecycleStartAndStopObserver<T : Any?>(
private val initializer: (() -> T)?
) : LifecycleAutoInitializer<T> {

View File

@ -31,9 +31,8 @@ allprojects {
maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
dependsOn(":common:data-models:generateI18n4kFiles")
kotlinOptions { jvmTarget = "1.8" }
}
afterEvaluate {
project.extensions.findByType<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension>()?.let { kmpExt ->

View File

@ -19,26 +19,14 @@ plugins {
id("org.jlleitschuh.gradle.ktlint-idea")
}
subprojects {
apply(plugin = "org.jlleitschuh.gradle.ktlint")
apply(plugin = "org.jlleitschuh.gradle.ktlint-idea")
repositories {
// Required to download KtLint
mavenCentral()
ktlint {
outputToConsole.set(true)
ignoreFailures.set(true)
coloredOutput.set(true)
verbose.set(true)
disabledRules.set(setOf("filename,no-wildcard-imports"))
filter {
exclude("**/generated/**")
exclude("**/build/**")
}
ktlint {
android.set(true)
outputToConsole.set(true)
ignoreFailures.set(true)
coloredOutput.set(true)
verbose.set(true)
filter {
exclude("**/generated/**")
exclude("**/build/**")
}
}
// Optionally configure plugin
/*configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
debug.set(true)
}*/
}

View File

@ -35,7 +35,7 @@ import com.shabinder.common.uikit.RazorPay
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
import com.shabinder.common.uikit.configurations.colorAccent
typealias DonationDialogCallBacks = Triple<openAction,dismissAction,snoozeAction>
typealias DonationDialogCallBacks = Triple<openAction, dismissAction, snoozeAction>
internal typealias openAction = () -> Unit
internal typealias dismissAction = () -> Unit
private typealias snoozeAction = () -> Unit
@ -58,7 +58,7 @@ fun DonationDialogComponent(onDismissExtra: () -> Unit): DonationDialogCallBacks
onDismissExtra()
isDonationDialogVisible = false
}
return DonationDialogCallBacks(openDonationDialog,dismissDonationDialog,snoozeDonationDialog)
return DonationDialogCallBacks(openDonationDialog, dismissDonationDialog, snoozeDonationDialog)
}
@ExperimentalAnimationApi
@ -68,7 +68,7 @@ fun DonationDialog(
onDismiss: () -> Unit,
onSnooze: () -> Unit
) {
Dialog(isVisible,onDismiss) {
Dialog(isVisible, onDismiss) {
Card(
modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, Color.Gray) // Gray

View File

@ -31,7 +31,7 @@ import com.shabinder.common.uikit.Dialog
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
import com.shabinder.common.uikit.configurations.colorAccent
typealias ErrorInfoDialogCallBacks = Pair<openAction,dismissAction>
typealias ErrorInfoDialogCallBacks = Pair<openAction, dismissAction>
@Composable
fun ErrorInfoDialog(error: Throwable): ErrorInfoDialogCallBacks {
@ -75,5 +75,5 @@ fun ErrorInfoDialog(error: Throwable): ErrorInfoDialogCallBacks {
}
}
return ErrorInfoDialogCallBacks(openErrorDialog,onDismissDialog)
return ErrorInfoDialogCallBacks(openErrorDialog, onDismissDialog)
}

View File

@ -121,7 +121,7 @@ fun SpotiFlyerListContent(
)
// Donation Dialog Visibility
val (openDonationDialog,dismissDonationDialog,snoozeDonationDialog) = DonationDialogComponent {
val (openDonationDialog, dismissDonationDialog, snoozeDonationDialog) = DonationDialogComponent {
component.dismissDonationDialogSetOffset()
}
@ -184,11 +184,14 @@ fun TrackCard(
CircularProgressIndicator()
}
is DownloadStatus.Failed -> {
val (openErrorDialog,dismissErrorDialog) = ErrorInfoDialog((track.downloaded as DownloadStatus.Failed).error)
val (openErrorDialog, dismissErrorDialog) = ErrorInfoDialog((track.downloaded as DownloadStatus.Failed).error)
Icon(Icons.Rounded.Info,Strings.downloadError(),tint = lightGray,modifier = Modifier.size(42.dp).clickable {
openErrorDialog()
}.padding(start = 4.dp,end = 12.dp))
Icon(
Icons.Rounded.Info, Strings.downloadError(), tint = lightGray,
modifier = Modifier.size(42.dp).clickable {
openErrorDialog()
}.padding(start = 4.dp, end = 12.dp)
)
DownloadImageError(
Modifier.clickable(

View File

@ -106,7 +106,7 @@ import com.shabinder.common.uikit.rememberScrollbarAdapter
fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
val model by component.model.subscribeAsState()
val (openDonationDialog,_,_) = DonationDialogComponent {
val (openDonationDialog, _, _) = DonationDialogComponent {
component.dismissDonationDialogOffset()
}
@ -253,7 +253,7 @@ fun SearchPanel(
@Composable
fun AboutColumn(
modifier: Modifier = Modifier,
analyticsEnabled:Boolean,
analyticsEnabled: Boolean,
openDonationDialog: () -> Unit,
toggleAnalytics: (enabled: Boolean) -> Unit
) {

View File

@ -82,7 +82,7 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
save()
}
)
.padding(horizontal = 16.dp,vertical = 2.dp)
.padding(horizontal = 16.dp, vertical = 2.dp)
) {
RadioButton(
selected = (quality == model.preferredQuality),
@ -98,7 +98,6 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
)
}
}
}
Spacer(Modifier.padding(top = 12.dp))
@ -157,7 +156,6 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
}
Spacer(modifier = Modifier.padding(top = 8.dp))
}
}
@OptIn(ExperimentalAnimationApi::class)
@ -165,7 +163,7 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
fun SettingsRow(
icon: Painter,
title: String,
value:String,
value: String,
editContent: @Composable ColumnScope.(() -> Unit) -> Unit
) {

View File

@ -4,7 +4,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font
actual fun montserratFont() = FontFamily(
Font("font/montserrat_light.ttf", FontWeight.Light),
Font("font/montserrat_regular.ttf", FontWeight.Normal),

View File

@ -29,9 +29,9 @@ val statelyVersion = "1.1.7"
val statelyIsoVersion = "1.1.7-a1"
i18n4k {
inputDirectory = "../../translations"
packageName = "com.shabinder.common.translations"
// sourceCodeLocales = listOf("en", "de", "es", "fr", "id", "pt", "ru", "uk")
inputDirectory = "../../translations"
packageName = "com.shabinder.common.translations"
// sourceCodeLocales = listOf("en", "de", "es", "fr", "id", "pt", "ru", "uk")
}
kotlin {

View File

@ -1,3 +1,3 @@
package com.shabinder.common
fun <T: Any?> T?.requireNotNull() : T = requireNotNull(this)
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)

View File

@ -2,42 +2,42 @@ package com.shabinder.common.models
import com.shabinder.common.translations.Strings
sealed class SpotiFlyerException(override val message: String): Exception(message) {
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 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,
val extraInfo: String? = null,
override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo"
): SpotiFlyerException(message)
) : SpotiFlyerException(message)
data class UnknownReason(
val exception: Throwable? = null,
override val message: String = Strings.unknownError()
): SpotiFlyerException(message)
) : SpotiFlyerException(message)
data class NoMatchFound(
val trackName: String? = null,
override val message: String = "$trackName : ${Strings.noMatchFound()}"
): SpotiFlyerException(message)
) : SpotiFlyerException(message)
data class YoutubeLinkNotFound(
val videoID: String? = null,
override val message: String = "${Strings.noLinkFound()}: $videoID"
): SpotiFlyerException(message)
) : SpotiFlyerException(message)
data class DownloadLinkFetchFailed(
val trackName: String,
val jioSaavnError: Throwable,
val ytMusicError: Throwable,
override val message: String = "${Strings.noLinkFound()}: $trackName," +
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
): SpotiFlyerException(message)
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
) : SpotiFlyerException(message)
data class LinkInvalid(
val link: String? = null,
override val message: String = "${Strings.linkNotValid()}\n ${link ?: ""}"
): SpotiFlyerException(message)
) : SpotiFlyerException(message)
}

View File

@ -130,8 +130,7 @@ inline fun <V, E : Throwable> Event<V, E>.unwrap(failure: (E) -> Nothing): V =
inline fun <V, E : Throwable> Event<V, E>.unwrapError(success: (V) -> Nothing): E =
apply { component1()?.let(success) }.component2()!!
sealed class Event<out V : Any?, out E : Throwable>: ReadOnlyProperty<Any?, V> {
sealed class Event<out V : Any?, out E : Throwable> : ReadOnlyProperty<Any?, V> {
open operator fun component1(): V? = null
open operator fun component2(): E? = null

View File

@ -19,7 +19,7 @@ infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.or(fallback: V) = whe
else -> SuspendableEvent.Success(fallback)
}
suspend inline infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.getOrElse(crossinline fallback:suspend (E) -> V): V {
suspend inline infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.getOrElse(crossinline fallback: suspend (E) -> V): V {
return when (this) {
is SuspendableEvent.Success -> value
is SuspendableEvent.Failure -> fallback(error)
@ -93,7 +93,6 @@ suspend inline fun <V : Any?, U : Any> SuspendableEvent<V, *>.fanout(
): SuspendableEvent<Pair<V, U>, *> =
flatMap { outer -> other().map { outer to it } }
suspend fun <V : Any?, E : Throwable> List<SuspendableEvent<V, E>>.lift(): SuspendableEvent<List<V>, E> = fold(
SuspendableEvent.Success<MutableList<V>, E>(mutableListOf<V>()) as SuspendableEvent<MutableList<V>, E>
) { acc, result ->
@ -102,7 +101,7 @@ suspend fun <V : Any?, E : Throwable> List<SuspendableEvent<V, E>>.lift(): Suspe
}
}
sealed class SuspendableEvent<out V : Any?, out E : Throwable>: ReadOnlyProperty<Any?,V> {
sealed class SuspendableEvent<out V : Any?, out E : Throwable> : ReadOnlyProperty<Any?, V> {
abstract operator fun component1(): V?
abstract operator fun component2(): E?
@ -156,7 +155,7 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable>: ReadOnlyProperty
// Factory methods
fun <E : Throwable> error(ex: E) = Failure<Nothing, E>(ex)
inline fun <V : Any?> of(value: V?,crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent<V, Throwable> {
inline fun <V : Any?> of(value: V?, crossinline fail: (() -> Throwable) = { Throwable() }): SuspendableEvent<V, Throwable> {
return value?.let { Success<V, Nothing>(it) } ?: error(fail())
}
@ -172,5 +171,4 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable>: ReadOnlyProperty
crossinline block: suspend () -> V
): SuspendableEvent<V, Throwable> = of(block)
}
}

View File

@ -5,5 +5,4 @@ class SuspendedValidation<out E : Throwable>(vararg resultSequence: SuspendableE
val failures: List<E> = resultSequence.filterIsInstance<SuspendableEvent.Failure<*, E>>().map { it.getThrowable() }
val hasFailure = failures.isNotEmpty()
}

View File

@ -28,9 +28,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.lang.Exception
import java.net.InetSocketAddress
import java.net.URL
import javax.net.ssl.HttpsURLConnection

View File

@ -40,7 +40,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
album = track.albumName
year = track.year
comment = "Genres:${track.comment}"
if(track.trackNumber != null)
if (track.trackNumber != null)
this.track = track.trackNumber.toString()
}
this.id3v1Tag = id3v1Tag
@ -60,7 +60,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
comment = track.comment
lyrics = track.lyrics ?: ""
url = track.trackUrl
if(track.trackNumber != null)
if (track.trackNumber != null)
this.track = track.trackNumber.toString()
}
try {

View File

@ -59,7 +59,7 @@ fun Dir.createDirectories() {
createDirectory(defaultDir() + "Albums/")
createDirectory(defaultDir() + "Playlists/")
createDirectory(defaultDir() + "YT_Downloads/")
} catch (e:Exception){}
} catch (e: Exception) {}
}
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =

View File

@ -58,7 +58,7 @@ class FetchPlatformQueryResult(
suspend fun authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient()
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult,Throwable> {
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult, Throwable> {
val result = when {
// SPOTIFY
link.contains("spotify", true) ->
@ -94,17 +94,17 @@ class FetchPlatformQueryResult(
suspend fun findMp3DownloadLink(
track: TrackDetails,
preferredQuality: AudioQuality = preferenceManager.audioQuality
): SuspendableEvent<String,Throwable> =
): SuspendableEvent<String, Throwable> =
if (track.videoID != null) {
// We Already have VideoID
when (track.source) {
Source.JioSaavn -> {
saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song ->
song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findMp3Link(track,preferredQuality)
song.media_url?.let { audioToMp3.convertToMp3(it) } ?: findMp3Link(track, preferredQuality)
}
}
Source.YouTube -> {
youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull(),preferredQuality).flatMapError {
youtubeMp3.getMp3DownloadLink(track.videoID.requireNotNull(), preferredQuality).flatMapError {
logger.e("Yt1sMp3 Failed") { it.message ?: "couldn't fetch link for ${track.videoID} ,trying manual extraction" }
youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink ->
audioToMp3.convertToMp3(m4aLink)
@ -113,17 +113,17 @@ class FetchPlatformQueryResult(
}
else -> {
/*We should never reach here for now*/
findMp3Link(track,preferredQuality)
findMp3Link(track, preferredQuality)
}
}
} else {
findMp3Link(track,preferredQuality)
findMp3Link(track, preferredQuality)
}
private suspend fun findMp3Link(
track: TrackDetails,
preferredQuality: AudioQuality
):SuspendableEvent<String,Throwable> {
): SuspendableEvent<String, Throwable> {
// Try Fetching Track from Jio Saavn
return saavnProvider.findMp3SongDownloadURL(
trackName = track.title,
@ -132,11 +132,11 @@ class FetchPlatformQueryResult(
).flatMapError { saavnError ->
logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
// Saavn Failed, Lets Try Fetching Now From Youtube Music
youtubeMusic.findMp3SongDownloadURLYT(track,preferredQuality).flatMapError { ytMusicError ->
youtubeMusic.findMp3SongDownloadURLYT(track, preferredQuality).flatMapError { ytMusicError ->
// If Both Failed Bubble the Exception Up with both StackTraces
SuspendableEvent.error(
SpotiFlyerException.DownloadLinkFetchFailed(
trackName = track.title,
trackName = track.title,
ytMusicError = ytMusicError,
jioSaavnError = saavnError
)

View File

@ -3,7 +3,7 @@ package com.shabinder.common.di.preference
import com.russhwolf.settings.Settings
import com.shabinder.common.models.AudioQuality
class PreferenceManager(settings: Settings): Settings by settings {
class PreferenceManager(settings: Settings) : Settings by settings {
companion object {
const val DIR_KEY = "downloadDir"
@ -17,14 +17,13 @@ class PreferenceManager(settings: Settings): Settings by settings {
val isAnalyticsEnabled get() = getBooleanOrNull(ANALYTICS_KEY) ?: false
fun toggleAnalytics(enabled: Boolean) = putBoolean(ANALYTICS_KEY, enabled)
/* DOWNLOAD DIRECTORY */
val downloadDir get() = getStringOrNull(DIR_KEY)
fun setDownloadDirectory(newBasePath: String) = putString(DIR_KEY, newBasePath)
/* Preferred Audio Quality */
val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320")
fun setPreferredAudioQuality(quality: AudioQuality) = putString(PREFERRED_AUDIO_QUALITY,quality.kbps)
fun setPreferredAudioQuality(quality: AudioQuality) = putString(PREFERRED_AUDIO_QUALITY, quality.kbps)
/* OFFSET FOR WHEN TO ASK FOR SUPPORT */
val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
@ -33,7 +32,6 @@ class PreferenceManager(settings: Settings): Settings by settings {
}
fun setDonationOffset(offset: Int = 5) = putInt(DONATION_INTERVAL, offset)
/* TO CHECK IF THIS IS APP's FIRST LAUNCH */
val isFirstLaunch get() = getBooleanOrNull(FIRST_LAUNCH) ?: true
fun firstLaunchDone() = putBoolean(FIRST_LAUNCH, false)

View File

@ -37,7 +37,7 @@ class GaanaProvider(
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
// Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")

View File

@ -33,7 +33,7 @@ class SaavnProvider(
).apply {
val pageLink = fullLink.substringAfter("saavn.com/").substringBefore("?")
when {
pageLink.contains("/song/",true) -> {
pageLink.contains("/song/", true) -> {
getSong(fullLink).value.let {
folderType = "Tracks"
subFolder = ""
@ -42,7 +42,7 @@ class SaavnProvider(
coverUrl = it.image.replace("http:", "https:")
}
}
pageLink.contains("/album/",true) -> {
pageLink.contains("/album/", true) -> {
getAlbum(fullLink).value.let {
folderType = "Albums"
subFolder = removeIllegalChars(it.title)
@ -51,7 +51,7 @@ class SaavnProvider(
coverUrl = it.image.replace("http:", "https:")
}
}
pageLink.contains("/featured/",true) -> { // Playlist
pageLink.contains("/featured/", true) -> { // Playlist
getPlaylist(fullLink).value.let {
folderType = "Playlists"
subFolder = removeIllegalChars(it.listname)

View File

@ -24,7 +24,7 @@ import com.shabinder.common.models.event.coroutines.SuspendableEvent
import com.shabinder.common.models.event.coroutines.map
import io.ktor.client.*
interface YoutubeMp3: Yt1sMp3 {
interface YoutubeMp3 : Yt1sMp3 {
companion object {
operator fun invoke(
@ -38,7 +38,7 @@ interface YoutubeMp3: Yt1sMp3 {
}
}
suspend fun getMp3DownloadLink(videoID: String,quality: AudioQuality): SuspendableEvent<String,Throwable> = getLinkFromYt1sMp3(videoID,quality).map {
suspend fun getMp3DownloadLink(videoID: String, quality: AudioQuality): SuspendableEvent<String, Throwable> = getLinkFromYt1sMp3(videoID, quality).map {
corsApi + it
}
}

View File

@ -63,7 +63,7 @@ class YoutubeMusic constructor(
): SuspendableEvent<String, Throwable> {
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
// As YT compress Audio hence there is no benefit of quality for more than 192
val optimalQuality = if((preferredQuality.kbps.toIntOrNull() ?: 0) > 192) AudioQuality.KBPS192 else preferredQuality
val optimalQuality = if ((preferredQuality.kbps.toIntOrNull() ?: 0) > 192) AudioQuality.KBPS192 else preferredQuality
// 1 Try getting Link from Yt1s
youtubeMp3.getMp3DownloadLink(videoID, optimalQuality).flatMapError {
// 2 if Yt1s failed , Extract Manually
@ -79,7 +79,7 @@ class YoutubeMusic constructor(
private suspend fun getYTIDBestMatch(
trackDetails: TrackDetails
):SuspendableEvent<String,Throwable> =
): SuspendableEvent<String, Throwable> =
getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList ->
sortByBestMatch(
matchList,
@ -89,7 +89,7 @@ class YoutubeMusic constructor(
).keys.firstOrNull() ?: throw SpotiFlyerException.NoMatchFound(trackDetails.title)
}
private suspend fun getYTTracks(query: String): SuspendableEvent<List<YoutubeTrack>,Throwable> =
private suspend fun getYTTracks(query: String): SuspendableEvent<List<YoutubeTrack>, Throwable> =
getYoutubeMusicResponse(query).map { youtubeResponseData ->
val youtubeTracks = mutableListOf<YoutubeTrack>()
val responseObj = Json.parseToJsonElement(youtubeResponseData)
@ -233,9 +233,9 @@ class YoutubeMusic constructor(
}
}
}
// logger.d {youtubeTracks.joinToString("\n")}
youtubeTracks
}
// logger.d {youtubeTracks.joinToString("\n")}
youtubeTracks
}
private fun sortByBestMatch(
ytTracks: List<YoutubeTrack>,
@ -311,7 +311,7 @@ class YoutubeMusic constructor(
}
}
private suspend fun getYoutubeMusicResponse(query: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
private suspend fun getYoutubeMusicResponse(query: String): SuspendableEvent<String, Throwable> = SuspendableEvent {
httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
contentType(ContentType.Application.Json)
headers {

View File

@ -51,7 +51,7 @@ class YoutubeProvider(
private val sampleDomain2 = "youtube.com"
private val sampleDomain3 = "youtu.be"
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult,Throwable> {
suspend fun query(fullLink: String): SuspendableEvent<PlatformQueryResult, Throwable> {
val link = fullLink.removePrefix("https://").removePrefix("http://")
if (link.contains("playlist", true) || link.contains("list", true)) {
// Given Link is of a Playlist
@ -86,7 +86,7 @@ class YoutubeProvider(
private suspend fun getYTPlaylist(
searchId: String
): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
PlatformQueryResult(
folderType = "",
subFolder = "",
@ -102,7 +102,7 @@ class YoutubeProvider(
val videos = playlist.videos
coverUrl = "https://i.ytimg.com/vi/${
videos.firstOrNull()?.videoId
videos.firstOrNull()?.videoId
}/hqdefault.jpg"
title = name
@ -116,11 +116,11 @@ class YoutubeProvider(
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()
)
itemName = it.title ?: "N/A",
type = folderType,
subFolder = subFolder,
dir.defaultDir()
)
)
)
DownloadStatus.Downloaded
@ -137,7 +137,7 @@ class YoutubeProvider(
@Suppress("DefaultLocale")
private suspend fun getYTTrack(
searchId: String,
): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
PlatformQueryResult(
folderType = "",
subFolder = "",
@ -162,11 +162,11 @@ class YoutubeProvider(
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (dir.isPresent(
dir.finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder,
defaultDir = dir.defaultDir()
)
itemName = name,
type = folderType,
subFolder = subFolder,
defaultDir = dir.defaultDir()
)
)
)
DownloadStatus.Downloaded

View File

@ -32,10 +32,10 @@ interface AudioToMp3 {
suspend fun convertToMp3(
URL: String,
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): SuspendableEvent<String,Throwable> = SuspendableEvent {
): SuspendableEvent<String, Throwable> = 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
var (activeHost, jobLink) = convertRequest(URL, audioQuality).value
// (jobStatus.contains("d")) == COMPLETION
var jobStatus: String
@ -48,7 +48,7 @@ interface AudioToMp3 {
)
} catch (e: Exception) {
e.printStackTrace()
if(e is ClientRequestException && e.response.status.value == 404) {
if (e is ClientRequestException && e.response.status.value == 404) {
// No Need to Retry, Host/Converter is Busy
throw SpotiFlyerException.MP3ConversionFailed(e.message)
}
@ -74,7 +74,7 @@ interface AudioToMp3 {
private suspend fun convertRequest(
URL: String,
audioQuality: AudioQuality = AudioQuality.KBPS160,
): SuspendableEvent<Pair<String,String>,Throwable> = SuspendableEvent {
): SuspendableEvent<Pair<String, String>, Throwable> = SuspendableEvent {
val activeHost by getHost()
val convertJob = client.submitFormWithBinaryData<String>(
url = activeHost,
@ -104,17 +104,17 @@ interface AudioToMp3 {
}.execute()
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
Pair(activeHost,convertJob)
Pair(activeHost, convertJob)
}
// Active Host free to process conversion
// ex - https://hostveryfast.onlineconverter.com/file/send
private suspend fun getHost(): SuspendableEvent<String,Throwable> = SuspendableEvent {
private suspend fun getHost(): SuspendableEvent<String, Throwable> = SuspendableEvent {
client.get<String>("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

View File

@ -25,7 +25,6 @@ import com.shabinder.common.models.gaana.GaanaSong
import io.ktor.client.*
import io.ktor.client.request.*
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
private val BASE_URL get() = "${corsApi}https://api.gaana.com"

View File

@ -40,12 +40,12 @@ interface JioSaavnRequests {
trackName: String,
trackArtists: List<String>,
preferredQuality: AudioQuality
): SuspendableEvent<String,Throwable> = searchForSong(trackName).map { songs ->
): SuspendableEvent<String, Throwable> = searchForSong(trackName).map { songs ->
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
val m4aLink: String by getSongFromID(bestMatches.keys.first()).map { song ->
val optimalQuality = if(song.is320Kbps && ((preferredQuality.kbps.toIntOrNull() ?: 0) > 160)) AudioQuality.KBPS320 else AudioQuality.KBPS160
song.media_url.requireNotNull().replaceAfterLast("_","${optimalQuality.kbps}.mp4")
val optimalQuality = if (song.is320Kbps && ((preferredQuality.kbps.toIntOrNull() ?: 0) > 160)) AudioQuality.KBPS320 else AudioQuality.KBPS160
song.media_url.requireNotNull().replaceAfterLast("_", "${optimalQuality.kbps}.mp4")
}
val mp3Link by audioToMp3.convertToMp3(m4aLink)
@ -56,7 +56,7 @@ interface JioSaavnRequests {
suspend fun searchForSong(
query: String,
includeLyrics: Boolean = false
): SuspendableEvent<List<SaavnSearchResult>,Throwable> = SuspendableEvent {
): SuspendableEvent<List<SaavnSearchResult>, Throwable> = SuspendableEvent {
val searchURL = search_base_url + query
val results = mutableListOf<SaavnSearchResult>()
@ -67,12 +67,12 @@ interface JioSaavnRequests {
(it as JsonObject).formatData().let { jsonObject ->
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
}
}
}
results
}
suspend fun getLyrics(ID: String): SuspendableEvent<String,Throwable> = SuspendableEvent {
suspend fun getLyrics(ID: String): SuspendableEvent<String, Throwable> = SuspendableEvent {
(Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
.getString("lyrics").requireNotNull()
}
@ -80,7 +80,7 @@ interface JioSaavnRequests {
suspend fun getSong(
URL: String,
fetchLyrics: Boolean = false
): SuspendableEvent<SaavnSong,Throwable> = SuspendableEvent {
): SuspendableEvent<SaavnSong, Throwable> = SuspendableEvent {
val id = getSongID(URL)
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
.formatData(fetchLyrics)
@ -91,7 +91,7 @@ interface JioSaavnRequests {
suspend fun getSongFromID(
ID: String,
fetchLyrics: Boolean = false
): SuspendableEvent<SaavnSong,Throwable> = SuspendableEvent {
): SuspendableEvent<SaavnSong, Throwable> = SuspendableEvent {
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
.formatData(fetchLyrics)
@ -112,7 +112,7 @@ interface JioSaavnRequests {
suspend fun getPlaylist(
URL: String,
includeLyrics: Boolean = false
): SuspendableEvent<SaavnPlaylist,Throwable> = SuspendableEvent {
): SuspendableEvent<SaavnPlaylist, Throwable> = SuspendableEvent {
globalJson.decodeFromJsonElement(
SaavnPlaylist.serializer(),
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject)
@ -122,7 +122,7 @@ interface JioSaavnRequests {
private suspend fun getPlaylistID(
URL: String
): SuspendableEvent<String,Throwable> = SuspendableEvent {
): SuspendableEvent<String, Throwable> = SuspendableEvent {
val res = httpClient.get<String>(URL)
try {
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
@ -134,7 +134,7 @@ interface JioSaavnRequests {
suspend fun getAlbum(
URL: String,
includeLyrics: Boolean = false
): SuspendableEvent<SaavnAlbum,Throwable> = SuspendableEvent {
): SuspendableEvent<SaavnAlbum, Throwable> = SuspendableEvent {
globalJson.decodeFromJsonElement(
SaavnAlbum.serializer(),
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject)
@ -144,7 +144,7 @@ interface JioSaavnRequests {
private suspend fun getAlbumID(
URL: String
): SuspendableEvent<String,Throwable> = SuspendableEvent {
): SuspendableEvent<String, Throwable> = SuspendableEvent {
val res = httpClient.get<String>(URL)
try {
res.split("\"album_id\":\"")[1].split('"')[0]

View File

@ -31,7 +31,7 @@ import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlin.native.concurrent.SharedImmutable
suspend fun authenticateSpotify(): SuspendableEvent<TokenData,Throwable> = SuspendableEvent {
suspend fun authenticateSpotify(): SuspendableEvent<TokenData, Throwable> = SuspendableEvent {
if (methods.value.isInternetAvailable) {
spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })

View File

@ -43,7 +43,7 @@ interface Yt1sMp3 {
/*
* Downloadable Mp3 Link for YT videoID.
* */
suspend fun getLinkFromYt1sMp3(videoID: String,quality: AudioQuality): SuspendableEvent<String,Throwable> = getKey(videoID,quality).flatMap { key ->
suspend fun getLinkFromYt1sMp3(videoID: String, quality: AudioQuality): SuspendableEvent<String, Throwable> = getKey(videoID, quality).flatMap { key ->
getConvertedMp3Link(videoID, key).map {
it["dlink"].requireNotNull()
.jsonPrimitive.content.replace("\"", "")
@ -54,7 +54,7 @@ interface Yt1sMp3 {
* POST:https://yt1s.com/api/ajaxSearch/index
* Body Form= q:yt video link ,vt:format=mp3
* */
private suspend fun getKey(videoID: String,quality: AudioQuality): SuspendableEvent<String,Throwable> = SuspendableEvent {
private suspend fun getKey(videoID: String, quality: AudioQuality): SuspendableEvent<String, Throwable> = SuspendableEvent {
val response: JsonObject = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
body = FormDataContent(
Parameters.build {
@ -67,7 +67,7 @@ interface Yt1sMp3 {
val mp3Keys = response.getJsonObject("links")
.getJsonObject("mp3")
val requestedKBPS = when(quality) {
val requestedKBPS = when (quality) {
AudioQuality.KBPS128 -> "mp3128"
else -> quality.kbps
}
@ -77,7 +77,7 @@ interface Yt1sMp3 {
specificQualityKey?.get("k").requireNotNull().jsonPrimitive.content
}
private suspend fun getConvertedMp3Link(videoID: String, key: String): SuspendableEvent<JsonObject,Throwable> = SuspendableEvent {
private suspend fun getConvertedMp3Link(videoID: String, key: String): SuspendableEvent<JsonObject, Throwable> = SuspendableEvent {
httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
body = FormDataContent(
Parameters.build {

View File

@ -165,7 +165,7 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<
componentContext = componentContext,
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
override val mainOutput: Consumer<SpotiFlyerMain.Output> = output
override val mainAnalytics = object : SpotiFlyerMain.Analytics , Analytics by analytics {}
override val mainAnalytics = object : SpotiFlyerMain.Analytics, Analytics by analytics {}
}
)

View File

@ -28,7 +28,6 @@ dependencies {
implementation(project(":common:list"))
implementation(project(":common:list"))
// Decompose
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)

View File

@ -20,6 +20,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("ktlint-setup")
}
group = "com.shabinder"

View File

@ -56,9 +56,8 @@ import java.net.URI
import javax.swing.JFileChooser
import javax.swing.JFileChooser.APPROVE_OPTION
private val koin = initKoin(enableNetworkLogs = true).koin
private lateinit var showToast: (String)->Unit
private lateinit var showToast: (String) -> Unit
private val tracker: PiwikTracker by lazy {
PiwikTracker("https://matomo.spotiflyer.ml/matomo.php")
}
@ -68,7 +67,7 @@ fun main() {
val lifecycle = LifecycleRegistry()
lifecycle.resume()
Window("SpotiFlyer",size = IntSize(450,800)) {
Window("SpotiFlyer", size = IntSize(450, 800)) {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.Black,
@ -80,7 +79,7 @@ fun main() {
shapes = SpotiFlyerShapes
) {
val root: SpotiFlyerRoot = SpotiFlyerRootContent(rememberRootComponent(factory = ::spotiFlyerRoot))
showToast = root.callBacks::showToast
showToast = root.callBacks::showToast
}
}
}
@ -98,11 +97,11 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
override val database: Database? = dir.db
override val preferenceManager: PreferenceManager = koin.get()
override val downloadProgressFlow = DownloadProgressFlow
override val actions: Actions = object: Actions {
override val actions: Actions = object : Actions {
override val platformActions = object : PlatformActions {}
override fun showPopUpMessage(string: String, long: Boolean) {
if(::showToast.isInitialized){
if (::showToast.isInitialized) {
showToast(string)
}
}
@ -114,7 +113,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
when (fileChooser.showOpenDialog(AppManager.focusedWindow?.window)) {
APPROVE_OPTION -> {
val directory = fileChooser.selectedFile
if(directory.canWrite()){
if (directory.canWrite()) {
preferenceManager.setDownloadDirectory(directory.absolutePath)
callBack(directory.absolutePath)
showPopUpMessage("${Strings.setDownloadDirectory()} \n${dir.defaultDir()}")
@ -128,7 +127,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
}
}
override fun queryActiveTracks() {/**/}
override fun queryActiveTracks() { /**/ }
override fun giveDonation() {
openLink("https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button")
@ -143,22 +142,22 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
override fun openPlatform(packageID: String, platformLink: String) = openLink(platformLink)
fun openLink(link:String) {
fun openLink(link: String) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(URI(link))
}
}
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
override fun writeMp3Tags(trackDetails: TrackDetails) { /*IMPLEMENTED*/ }
override val isInternetAvailable: Boolean
get() = runBlocking {
get() = runBlocking {
isInternetAccessible()
}
}
override val analytics = object: SpotiFlyerRoot.Analytics {
override val analytics = object : SpotiFlyerRoot.Analytics {
override fun appLaunchEvent() {
if(preferenceManager.isFirstLaunch) {
if (preferenceManager.isFirstLaunch) {
// Enable Analytics on First Launch
preferenceManager.toggleAnalytics(true)
preferenceManager.firstLaunchDone()

View File

@ -4,9 +4,8 @@ import org.piwik.java.tracking.PiwikRequest
import org.piwik.java.tracking.PiwikTracker
import java.net.URL
fun PiwikTracker.trackAsync(
baseURL:String = "https://com.shabinder.spotiflyer/",
baseURL: String = "https://com.shabinder.spotiflyer/",
requestBuilder: PiwikRequest.() -> Unit = {}
) {
val req = PiwikRequest(
@ -18,7 +17,7 @@ fun PiwikTracker.trackAsync(
}
fun PiwikTracker.trackScreenAsync(
screenAddress:String,
screenAddress: String,
requestBuilder: PiwikRequest.() -> Unit = {}
) {
val req = PiwikRequest(

View File

@ -22,7 +22,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx3072m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

View File

@ -7,10 +7,10 @@ fun getUpdatedContent(
oldContent: String,
newInsertionText: String,
tagName: String
): String{
): String {
return getReplaceableRegex(tagName).replace(
oldContent,
getReplacementText(tagName,newInsertionText)
getReplacementText(tagName, newInsertionText)
)
}
@ -26,5 +26,5 @@ private fun getReplacementText(
${Common.START_SECTION(tagName)}
$newInsertionText
${Common.END_SECTION(tagName)}
""".trimIndent()
""".trimIndent()
}

View File

@ -19,7 +19,6 @@ internal object GithubService {
private const val baseURL = Common.GITHUB_API
suspend fun getGithubRepoReleasesInfo(
ownerName: String,
repoName: String,

View File

@ -25,8 +25,8 @@ fun main(args: Array<String>) {
updatedGithubContent,
secrets
)
} catch (e:Exception) {
debug("Analytics Image Updation Failed",e.message.toString())
} catch (e: Exception) {
debug("Analytics Image Updation Failed", e.message.toString())
}
// TASK -> Update Total Downloads Card
@ -35,8 +35,8 @@ fun main(args: Array<String>) {
updatedGithubContent,
secrets.copy(tagName = "DCI")
)
} catch (e:Exception) {
debug("Download Card Updation Failed",e.message.toString())
} catch (e: Exception) {
debug("Download Card Updation Failed", e.message.toString())
}
// Write New Updated README.md

View File

@ -1,7 +1,7 @@
package models.github
import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
data class Reactions(

View File

@ -54,13 +54,13 @@ internal suspend fun getAnalyticsImage(): String {
}
}
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
debug("Content Length for Analytics Image",contentLength.toString())
debug("Content Length for Analytics Image", contentLength.toString())
if(retryCount-- == 0){
if (retryCount-- == 0) {
// FAIL Gracefully
throw(RETRY_LIMIT_EXHAUSTED())
}
}while (contentLength<1_20_000)
} while (contentLength <1_20_000)
return analyticsImage
}

View File

@ -18,15 +18,15 @@ internal suspend fun updateDownloadCards(
fileName = "README.md"
).decryptedContent
var totalDownloads:Int = GithubService.getGithubRepoReleasesInfo(
var totalDownloads: Int = GithubService.getGithubRepoReleasesInfo(
secrets.ownerName,
secrets.repoName
).let { allReleases ->
var totalCount = 0
for(release in allReleases){
for (release in allReleases) {
release.assets.forEach {
//debug("${it.name}: ${release.tag_name}" ,"Downloads: ${it.download_count}")
// debug("${it.name}: ${release.tag_name}" ,"Downloads: ${it.download_count}")
totalCount += it.download_count
}
}
@ -75,19 +75,18 @@ private suspend fun getDownloadCard(
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
// debug(contentLength.toString())
if(retryCount-- == 0){
if (retryCount-- == 0) {
// FAIL Gracefully
throw(RETRY_LIMIT_EXHAUSTED())
}
}while (contentLength<40_000)
} while (contentLength <40_000)
return downloadCard
}
fun getDownloadCardHtml(
count: Int,
date: String, // ex: 06 Jun 2021
):String {
): String {
return """
<div class="card-container">
<div id="card" class="dark-bg">