mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-21 16:54:33 +01:00
Code Cleanup and Formatting and i18n4k dependency
This commit is contained in:
parent
c591842fb4
commit
116530cc3c
@ -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")
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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())
|
||||
|
@ -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 -> ""
|
||||
|
@ -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) }
|
||||
|
@ -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() }
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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?
|
||||
}
|
@ -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> {
|
||||
|
||||
|
@ -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> {
|
||||
|
||||
|
@ -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> {
|
||||
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
||||
}*/
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
@ -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(
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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
|
||||
) {
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -1,3 +1,3 @@
|
||||
package com.shabinder.common
|
||||
|
||||
fun <T: Any?> T?.requireNotNull() : T = requireNotNull(this)
|
||||
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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/")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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") })
|
||||
|
@ -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 {
|
||||
|
@ -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 {}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -28,7 +28,6 @@ dependencies {
|
||||
implementation(project(":common:list"))
|
||||
implementation(project(":common:list"))
|
||||
|
||||
|
||||
// Decompose
|
||||
implementation(Decompose.decompose)
|
||||
implementation(Decompose.extensionsCompose)
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
@ -19,7 +19,6 @@ internal object GithubService {
|
||||
|
||||
private const val baseURL = Common.GITHUB_API
|
||||
|
||||
|
||||
suspend fun getGithubRepoReleasesInfo(
|
||||
ownerName: String,
|
||||
repoName: String,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user