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")
|
kotlin("android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
id("org.jetbrains.compose")
|
id("org.jetbrains.compose")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.shabinder"
|
group = "com.shabinder"
|
||||||
@ -39,7 +40,7 @@ repositories {
|
|||||||
android {
|
android {
|
||||||
val props = gradleLocalProperties(rootDir)
|
val props = gradleLocalProperties(rootDir)
|
||||||
|
|
||||||
if(props.containsKey("storeFileDir")) {
|
if (props.containsKey("storeFileDir")) {
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
storeFile = file(props.getProperty("storeFileDir"))
|
storeFile = file(props.getProperty("storeFileDir"))
|
||||||
@ -65,7 +66,7 @@ android {
|
|||||||
getByName("release") {
|
getByName("release") {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
// isShrinkResources = true
|
// isShrinkResources = true
|
||||||
if(props.containsKey("storeFileDir")){
|
if (props.containsKey("storeFileDir")) {
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
@ -134,7 +135,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implementation(Extras.kermit)
|
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("dev.icerock.moko:parcelize:${Versions.mokoParcelize}")
|
||||||
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
|
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
|
||||||
implementation("com.google.accompanist:accompanist-insets:0.12.0")
|
implementation("com.google.accompanist:accompanist-insets:0.12.0")
|
||||||
@ -146,4 +147,4 @@ dependencies {
|
|||||||
|
|
||||||
// Desugaring
|
// Desugaring
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ import org.matomo.sdk.Matomo
|
|||||||
import org.matomo.sdk.Tracker
|
import org.matomo.sdk.Tracker
|
||||||
import org.matomo.sdk.TrackerBuilder
|
import org.matomo.sdk.TrackerBuilder
|
||||||
|
|
||||||
class App: Application(), KoinComponent {
|
class App : Application(), KoinComponent {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SIGNATURE_HEX = "53304f6d75736a2f30484230334c454b714753525763724259444d3d0a"
|
const val SIGNATURE_HEX = "53304f6d75736a2f30484230334c454b714753525763724259444d3d0a"
|
||||||
@ -42,7 +42,8 @@ class App: Application(), KoinComponent {
|
|||||||
|
|
||||||
val tracker: Tracker by lazy {
|
val tracker: Tracker by lazy {
|
||||||
TrackerBuilder.createDefault(
|
TrackerBuilder.createDefault(
|
||||||
"https://matomo.spotiflyer.ml/matomo.php", 1)
|
"https://matomo.spotiflyer.ml/matomo.php", 1
|
||||||
|
)
|
||||||
.build(Matomo.getInstance(this)).apply {
|
.build(Matomo.getInstance(this)).apply {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
/*Timber.plant(DebugTree())
|
/*Timber.plant(DebugTree())
|
||||||
@ -96,4 +97,4 @@ class App: Application(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,6 @@ import org.koin.android.ext.android.inject
|
|||||||
import org.matomo.sdk.extra.TrackHelper
|
import org.matomo.sdk.extra.TrackHelper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@ -167,7 +166,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
LaunchedEffect(view) {
|
LaunchedEffect(view) {
|
||||||
permissionGranted.value = checkPermissions()
|
permissionGranted.value = checkPermissions()
|
||||||
if(preferenceManager.isFirstLaunch) {
|
if (preferenceManager.isFirstLaunch) {
|
||||||
delay(2500)
|
delay(2500)
|
||||||
// Ask For Analytics Permission on first Dialog
|
// Ask For Analytics Permission on first Dialog
|
||||||
askForAnalyticsPermission = true
|
askForAnalyticsPermission = true
|
||||||
@ -187,8 +186,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
* and Track Downloads for all other releases like F-Droid,
|
* 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
|
* for `Github Downloads` we will track Downloads using : https://tooomm.github.io/github-release-stats/?username=Shabinder&repository=SpotiFlyer
|
||||||
* */
|
* */
|
||||||
if(isGithubRelease) { checkIfLatestVersion() }
|
if (isGithubRelease) { checkIfLatestVersion() }
|
||||||
if(preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
|
if (preferenceManager.isAnalyticsEnabled && !isGithubRelease) {
|
||||||
// Download/App Install Event for F-Droid builds
|
// Download/App Install Event for F-Droid builds
|
||||||
TrackHelper.track().download().with(tracker)
|
TrackHelper.track().download().with(tracker)
|
||||||
}
|
}
|
||||||
@ -248,7 +247,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
/*END: Foreground Service Handlers*/
|
/*END: Foreground Service Handlers*/
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun isInternetAvailableState(): State<Boolean?> {
|
private fun isInternetAvailableState(): State<Boolean?> {
|
||||||
return internetAvailability.observeAsState()
|
return internetAvailability.observeAsState()
|
||||||
@ -258,7 +256,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
string,
|
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()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -270,23 +268,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
|
private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
|
||||||
SpotiFlyerRoot(
|
SpotiFlyerRoot(
|
||||||
componentContext,
|
componentContext,
|
||||||
dependencies = object : SpotiFlyerRoot.Dependencies{
|
dependencies = object : SpotiFlyerRoot.Dependencies {
|
||||||
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
|
override val storeFactory = LoggingStoreFactory(DefaultStoreFactory)
|
||||||
override val database = this@MainActivity.dir.db
|
override val database = this@MainActivity.dir.db
|
||||||
override val fetchQuery = this@MainActivity.fetcher
|
override val fetchQuery = this@MainActivity.fetcher
|
||||||
override val dir: Dir = this@MainActivity.dir
|
override val dir: Dir = this@MainActivity.dir
|
||||||
override val preferenceManager = this@MainActivity.preferenceManager
|
override val preferenceManager = this@MainActivity.preferenceManager
|
||||||
override val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
|
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 platformActions = object : PlatformActions {
|
||||||
override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator
|
override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator
|
||||||
override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey,
|
override val sharedPreferences = applicationContext.getSharedPreferences(
|
||||||
|
SharedPreferencesKey,
|
||||||
MODE_PRIVATE
|
MODE_PRIVATE
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun addToLibrary(path: String) {
|
override fun addToLibrary(path: String) {
|
||||||
MediaScannerConnection.scanFile (
|
MediaScannerConnection.scanFile(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
listOf(path).toTypedArray(), null, null
|
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 setDownloadDirectoryAction(callBack: (String) -> Unit) = setUpOnPrefClickListener(callBack)
|
||||||
|
|
||||||
override fun queryActiveTracks() = this@MainActivity.queryActiveTracks()
|
override fun queryActiveTracks() = this@MainActivity.queryActiveTracks()
|
||||||
|
|
||||||
override fun giveDonation() {
|
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() {
|
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
|
* Analytics Will Only Be Sent if User Granted us the Permission
|
||||||
* */
|
* */
|
||||||
override val analytics = object: Analytics {
|
override val analytics = object : Analytics {
|
||||||
override fun appLaunchEvent() {
|
override fun appLaunchEvent() {
|
||||||
if(preferenceManager.isAnalyticsEnabled){
|
if (preferenceManager.isAnalyticsEnabled) {
|
||||||
TrackHelper.track()
|
TrackHelper.track()
|
||||||
.event("events","App_Launch")
|
.event("events", "App_Launch")
|
||||||
.name("App Launch").with(tracker)
|
.name("App Launch").with(tracker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun homeScreenVisit() {
|
override fun homeScreenVisit() {
|
||||||
if(preferenceManager.isAnalyticsEnabled){
|
if (preferenceManager.isAnalyticsEnabled) {
|
||||||
// HomeScreen Visit Event
|
// HomeScreen Visit Event
|
||||||
TrackHelper.track().screen("/main_activity/home_screen")
|
TrackHelper.track().screen("/main_activity/home_screen")
|
||||||
.title("HomeScreen").with(tracker)
|
.title("HomeScreen").with(tracker)
|
||||||
@ -368,7 +367,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun listScreenVisit() {
|
override fun listScreenVisit() {
|
||||||
if(preferenceManager.isAnalyticsEnabled){
|
if (preferenceManager.isAnalyticsEnabled) {
|
||||||
// ListScreen Visit Event
|
// ListScreen Visit Event
|
||||||
TrackHelper.track().screen("/main_activity/list_screen")
|
TrackHelper.track().screen("/main_activity/list_screen")
|
||||||
.title("ListScreen").with(tracker)
|
.title("ListScreen").with(tracker)
|
||||||
@ -400,15 +399,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun setUpOnPrefClickListener(callBack : (String) -> Unit) {
|
private fun setUpOnPrefClickListener(callBack: (String) -> Unit) {
|
||||||
// Initialize Builder
|
// Initialize Builder
|
||||||
val chooser = StorageChooser.Builder()
|
val chooser = StorageChooser.Builder()
|
||||||
.withActivity(this)
|
.withActivity(this)
|
||||||
.withFragmentManager(fragmentManager)
|
.withFragmentManager(fragmentManager)
|
||||||
.withMemoryBar(true)
|
.withMemoryBar(true)
|
||||||
.setTheme(StorageChooser.Theme(applicationContext).apply {
|
.setTheme(
|
||||||
scheme = applicationContext.resources.getIntArray(R.array.default_dark)
|
StorageChooser.Theme(applicationContext).apply {
|
||||||
})
|
scheme = applicationContext.resources.getIntArray(R.array.default_dark)
|
||||||
|
}
|
||||||
|
)
|
||||||
.setDialogTitle(Strings.setDownloadDirectory())
|
.setDialogTitle(Strings.setDownloadDirectory())
|
||||||
.allowCustomPath(true)
|
.allowCustomPath(true)
|
||||||
.setType(StorageChooser.DIRECTORY_CHOOSER)
|
.setType(StorageChooser.DIRECTORY_CHOOSER)
|
||||||
@ -423,7 +424,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
preferenceManager.setDownloadDirectory(path)
|
preferenceManager.setDownloadDirectory(path)
|
||||||
callBack(path)
|
callBack(path)
|
||||||
showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}"))
|
showPopUpMessage(Strings.downloadDirectorySetTo("\n${dir.defaultDir()}"))
|
||||||
}else{
|
} else {
|
||||||
showPopUpMessage(Strings.noWriteAccess("\n$path "))
|
showPopUpMessage(Strings.noWriteAccess("\n$path "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -445,7 +446,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Ignoring battery optimization
|
// Ignoring battery optimization
|
||||||
permissionGranted.value = true
|
permissionGranted.value = true
|
||||||
} else {
|
} 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 filterLinkRegex = """http.+\w""".toRegex()
|
||||||
val string = it.replace("\n".toRegex(), " ")
|
val string = it.replace("\n".toRegex(), " ")
|
||||||
val link = filterLinkRegex.find(string)?.value.toString()
|
val link = filterLinkRegex.find(string)?.value.toString()
|
||||||
Log.i("Intent",link)
|
Log.i("Intent", link)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
while(!this@MainActivity::root.isInitialized){
|
while (!this@MainActivity::root.isInitialized) {
|
||||||
delay(100)
|
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
|
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() {
|
class ForegroundService : LifecycleService() {
|
||||||
|
|
||||||
private var downloadService: AutoClear<ParallelExecutor> = autoClear { ParallelExecutor(Dispatchers.IO) }
|
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 fetcher: FetchPlatformQueryResult by inject()
|
||||||
private val logger: Kermit by inject()
|
private val logger: Kermit by inject()
|
||||||
private val dir: Dir by inject()
|
private val dir: Dir by inject()
|
||||||
@ -133,18 +133,18 @@ class ForegroundService : LifecycleService() {
|
|||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
trackList.forEach {
|
for (track in trackList) {
|
||||||
trackStatusFlowMap[it.title] = DownloadStatus.Queued
|
trackStatusFlowMap[track.title] = DownloadStatus.Queued
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
downloadService.value.execute {
|
downloadService.value.execute {
|
||||||
fetcher.findMp3DownloadLink(it).fold(
|
fetcher.findMp3DownloadLink(track).fold(
|
||||||
success = { url ->
|
success = { url ->
|
||||||
enqueueDownload(url, it)
|
enqueueDownload(url, track)
|
||||||
},
|
},
|
||||||
failure = { error ->
|
failure = { error ->
|
||||||
failed++
|
failed++
|
||||||
updateNotification()
|
updateNotification()
|
||||||
trackStatusFlowMap[it.title] = DownloadStatus.Failed(error)
|
trackStatusFlowMap[track.title] = DownloadStatus.Failed(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ class ForegroundService : LifecycleService() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
logger.d(TAG) { "Killing Self" }
|
logger.d(TAG) { "Killing Self" }
|
||||||
messageList = messageList.getEmpty().apply {
|
messageList = messageList.getEmpty().apply {
|
||||||
set(index = 0, Message(Strings.cleaningAndExiting(),DownloadStatus.NotDownloaded))
|
set(index = 0, Message(Strings.cleaningAndExiting(), DownloadStatus.NotDownloaded))
|
||||||
}
|
}
|
||||||
downloadService.getOrNull()?.close()
|
downloadService.getOrNull()?.close()
|
||||||
downloadService.reset()
|
downloadService.reset()
|
||||||
@ -260,7 +260,7 @@ class ForegroundService : LifecycleService() {
|
|||||||
setSmallIcon(R.drawable.ic_download_arrow)
|
setSmallIcon(R.drawable.ic_download_arrow)
|
||||||
setContentTitle("${Strings.total()}: $total ${Strings.completed()}:$converted ${Strings.failed()}:$failed")
|
setContentTitle("${Strings.total()}: $total ${Strings.completed()}:$converted ${Strings.failed()}:$failed")
|
||||||
setSilent(true)
|
setSilent(true)
|
||||||
setProgress(total,failed+converted,false)
|
setProgress(total, failed + converted, false)
|
||||||
setStyle(
|
setStyle(
|
||||||
NotificationCompat.InboxStyle().run {
|
NotificationCompat.InboxStyle().run {
|
||||||
addLine(messageList[messageList.size - 1].asString())
|
addLine(messageList[messageList.size - 1].asString())
|
||||||
|
@ -7,7 +7,7 @@ typealias Message = Pair<String, DownloadStatus>
|
|||||||
|
|
||||||
val Message.title: String get() = first
|
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) {
|
val Message.progress: String get() = when (downloadStatus) {
|
||||||
is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%"
|
is DownloadStatus.Downloading -> "-> ${(downloadStatus as DownloadStatus.Downloading).progress}%"
|
||||||
@ -18,12 +18,12 @@ val Message.progress: String get() = when (downloadStatus) {
|
|||||||
is DownloadStatus.NotDownloaded -> ""
|
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 ,
|
// `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
|
// all Progress data is emitted all together from fun
|
||||||
fun Message.asString(): String {
|
fun Message.asString(): String {
|
||||||
val statusString = when(downloadStatus){
|
val statusString = when (downloadStatus) {
|
||||||
is DownloadStatus.Downloading -> Strings.downloading()
|
is DownloadStatus.Downloading -> Strings.downloading()
|
||||||
is DownloadStatus.Converting -> Strings.processing()
|
is DownloadStatus.Converting -> Strings.processing()
|
||||||
else -> ""
|
else -> ""
|
||||||
@ -31,4 +31,4 @@ fun Message.asString(): String {
|
|||||||
return "$statusString $title ${""/*progress*/}".trim()
|
return "$statusString $title ${""/*progress*/}".trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<Message>.getEmpty(): MutableList<Message> = java.util.Collections.synchronizedList(MutableList(size) { emptyMessage })
|
fun List<Message>.getEmpty(): MutableList<Message> = java.util.Collections.synchronizedList(MutableList(size) { emptyMessage })
|
||||||
|
@ -6,12 +6,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TrackStatusFlowMap(
|
class TrackStatusFlowMap(
|
||||||
val statusFlow: MutableSharedFlow<HashMap<String,DownloadStatus>>,
|
val statusFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>,
|
||||||
private val scope: CoroutineScope
|
private val scope: CoroutineScope
|
||||||
): HashMap<String,DownloadStatus>() {
|
) : HashMap<String, DownloadStatus>() {
|
||||||
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
|
override fun put(key: String, value: DownloadStatus): DownloadStatus? {
|
||||||
val res = super.put(key, value)
|
val res = super.put(key, value)
|
||||||
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
|
scope.launch { statusFlow.emit(this@TrackStatusFlowMap) }
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import java.io.File
|
|||||||
**/
|
**/
|
||||||
fun cleanFiles(dir: File) {
|
fun cleanFiles(dir: File) {
|
||||||
try {
|
try {
|
||||||
Log.d("File Cleaning","Starting Cleaning in ${dir.path} ")
|
Log.d("File Cleaning", "Starting Cleaning in ${dir.path} ")
|
||||||
val fList = dir.listFiles()
|
val fList = dir.listFiles()
|
||||||
fList?.let {
|
fList?.let {
|
||||||
for (file in fList) {
|
for (file in fList) {
|
||||||
@ -16,7 +16,7 @@ fun cleanFiles(dir: File) {
|
|||||||
cleanFiles(file)
|
cleanFiles(file)
|
||||||
} else if (file.isFile) {
|
} else if (file.isFile) {
|
||||||
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
||||||
Log.d("Files Cleaning","Cleaning ${file.path}")
|
Log.d("Files Cleaning", "Cleaning ${file.path}")
|
||||||
file.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,4 +24,3 @@ fun cleanFiles(dir: File) {
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) { e.printStackTrace() }
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,8 +34,8 @@ import com.shabinder.common.uikit.configurations.colorPrimary
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun AnalyticsDialog(
|
fun AnalyticsDialog(
|
||||||
isVisible:Boolean,
|
isVisible: Boolean,
|
||||||
enableAnalytics: ()->Unit,
|
enableAnalytics: () -> Unit,
|
||||||
dismissDialog: () -> Unit,
|
dismissDialog: () -> Unit,
|
||||||
) {
|
) {
|
||||||
// Analytics Permission Dialog
|
// Analytics Permission Dialog
|
||||||
@ -43,10 +43,10 @@ fun AnalyticsDialog(
|
|||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = dismissDialog,
|
onDismissRequest = dismissDialog,
|
||||||
title = {
|
title = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,horizontalArrangement = Arrangement.SpaceEvenly) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Icon(Icons.Rounded.Insights,Strings.analytics(), Modifier.size(42.dp))
|
Icon(Icons.Rounded.Insights, Strings.analytics(), Modifier.size(42.dp))
|
||||||
Spacer(Modifier.padding(horizontal = 8.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,
|
backgroundColor = Color.DarkGray,
|
||||||
@ -60,7 +60,7 @@ fun AnalyticsDialog(
|
|||||||
shape = SpotiFlyerShapes.medium,
|
shape = SpotiFlyerShapes.medium,
|
||||||
colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF303030))
|
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))
|
Spacer(Modifier.padding(vertical = 4.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
@ -73,14 +73,14 @@ fun AnalyticsDialog(
|
|||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
shape = SpotiFlyerShapes.medium
|
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 = {
|
||||||
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
|
@Composable
|
||||||
fun NetworkDialog(
|
fun NetworkDialog(
|
||||||
networkAvailability: State<Boolean?>
|
networkAvailability: State<Boolean?>
|
||||||
){
|
) {
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit){
|
LaunchedEffect(Unit) {
|
||||||
delay(2600)
|
delay(2600)
|
||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
@ -75,21 +75,30 @@ fun NetworkDialog(
|
|||||||
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
|
Text("Retry",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center)
|
||||||
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
|
Icon(Icons.Rounded.SyncProblem,"Check Network Connection Again")
|
||||||
}
|
}
|
||||||
*/},
|
*/
|
||||||
title = { Text(Strings.noInternetConnection(),
|
},
|
||||||
style = SpotiFlyerTypography.h5,
|
title = {
|
||||||
textAlign = TextAlign.Center) },
|
Text(
|
||||||
|
Strings.noInternetConnection(),
|
||||||
|
style = SpotiFlyerTypography.h5,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
backgroundColor = Color.DarkGray,
|
backgroundColor = Color.DarkGray,
|
||||||
text = {
|
text = {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally,verticalArrangement = Arrangement.Center){
|
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||||
Spacer(modifier = Modifier.padding(8.dp))
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
|
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Image(Icons.Rounded.CloudOff,
|
Image(
|
||||||
Strings.noInternetConnection(),Modifier.size(42.dp),colorFilter = ColorFilter.tint(
|
Icons.Rounded.CloudOff,
|
||||||
colorOffWhite
|
Strings.noInternetConnection(), Modifier.size(42.dp),
|
||||||
))
|
colorFilter = ColorFilter.tint(
|
||||||
|
colorOffWhite
|
||||||
|
)
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = Strings.checkInternetConnection(),
|
text = Strings.checkInternetConnection(),
|
||||||
@ -97,8 +106,8 @@ fun NetworkDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
,shape = SpotiFlyerShapes.medium
|
shape = SpotiFlyerShapes.medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,9 @@ import kotlinx.coroutines.delay
|
|||||||
@Composable
|
@Composable
|
||||||
fun PermissionDialog(
|
fun PermissionDialog(
|
||||||
permissionGranted: Boolean,
|
permissionGranted: Boolean,
|
||||||
requestStoragePermission:() -> Unit,
|
requestStoragePermission: () -> Unit,
|
||||||
disableDozeMode:() -> Unit,
|
disableDozeMode: () -> Unit,
|
||||||
){
|
) {
|
||||||
var askForPermission by remember { mutableStateOf(false) }
|
var askForPermission by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(2600)
|
delay(2600)
|
||||||
@ -61,18 +61,20 @@ fun PermissionDialog(
|
|||||||
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
|
Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth()
|
||||||
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
|
.background(colorPrimary, shape = SpotiFlyerShapes.medium)
|
||||||
.padding(horizontal = 8.dp),
|
.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,
|
backgroundColor = Color.DarkGray,
|
||||||
text = {
|
text = {
|
||||||
Column{
|
Column {
|
||||||
Spacer(modifier = Modifier.padding(8.dp))
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp)
|
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))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -89,7 +91,7 @@ fun PermissionDialog(
|
|||||||
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.SystemSecurityUpdate,Strings.backgroundRunning())
|
Icon(Icons.Rounded.SystemSecurityUpdate, Strings.backgroundRunning())
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -123,4 +125,4 @@ fun PermissionDialog(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
|
||||||
import com.shabinder.spotiflyer.App
|
import com.shabinder.spotiflyer.App
|
||||||
import java.security.MessageDigest
|
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", "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()}")
|
// 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()) {
|
if (App.SIGNATURE_HEX == currentSignature.toByteArray().toHEX()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
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
|
return false
|
||||||
}
|
}
|
||||||
@ -40,4 +39,4 @@ fun ByteArray.toHEX(): String {
|
|||||||
builder.append(String.format("%02x", aByte))
|
builder.append(String.format("%02x", aByte))
|
||||||
}
|
}
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,9 @@ import com.github.javiersantos.appupdater.enums.Display
|
|||||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||||
|
|
||||||
fun Activity.checkIfLatestVersion() {
|
fun Activity.checkIfLatestVersion() {
|
||||||
AppUpdater(this,0).run {
|
AppUpdater(this, 0).run {
|
||||||
setDisplay(Display.NOTIFICATION)
|
setDisplay(Display.NOTIFICATION)
|
||||||
showAppUpdated(false)//true:Show App is Updated Dialog
|
showAppUpdated(false) // true:Show App is Updated Dialog
|
||||||
setUpdateFrom(UpdateFrom.XML)
|
setUpdateFrom(UpdateFrom.XML)
|
||||||
setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/Compose/app/src/main/res/xml/app_update.xml")
|
setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/Compose/app/src/main/res/xml/app_update.xml")
|
||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
@ -42,24 +42,26 @@ fun Activity.checkIfLatestVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.checkPermissions():Boolean = ContextCompat
|
fun Activity.checkPermissions(): Boolean = ContextCompat
|
||||||
.checkSelfPermission(this,
|
.checkSelfPermission(
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
|
this,
|
||||||
&&
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
) == PackageManager.PERMISSION_GRANTED &&
|
||||||
ContextCompat.checkSelfPermission(this,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(
|
||||||
} else true
|
this,
|
||||||
|
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else true
|
||||||
|
|
||||||
@SuppressLint("BatteryLife", "ObsoleteSdkInt")
|
@SuppressLint("BatteryLife", "ObsoleteSdkInt")
|
||||||
fun Activity.disableDozeMode(requestCode:Int) {
|
fun Activity.disableDozeMode(requestCode: Int) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val pm =
|
val pm =
|
||||||
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
if (!isIgnoringBatteryOptimizations) {
|
if (!isIgnoringBatteryOptimizations) {
|
||||||
val intent = Intent().apply{
|
val intent = Intent().apply {
|
||||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
data = Uri.parse("package:$packageName")
|
data = Uri.parse("package:$packageName")
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package com.shabinder.spotiflyer.utils.autoclear
|
|||||||
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.shabinder.common.requireNotNull
|
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.AutoClear.Companion.TRIGGER
|
import com.shabinder.spotiflyer.utils.autoclear.AutoClear.Companion.TRIGGER
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleCreateAndDestroyObserver
|
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleCreateAndDestroyObserver
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleResumeAndPauseObserver
|
import com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers.LifecycleResumeAndPauseObserver
|
||||||
@ -34,7 +33,7 @@ class AutoClear<T : Any?>(
|
|||||||
fun getOrNull(): T? = _value
|
fun getOrNull(): T? = _value
|
||||||
|
|
||||||
private val observer: LifecycleAutoInitializer<T?> by lazy {
|
private val observer: LifecycleAutoInitializer<T?> by lazy {
|
||||||
when(trigger) {
|
when (trigger) {
|
||||||
TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer)
|
TRIGGER.ON_CREATE -> LifecycleCreateAndDestroyObserver(initializer)
|
||||||
TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer)
|
TRIGGER.ON_START -> LifecycleStartAndStopObserver(initializer)
|
||||||
TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer)
|
TRIGGER.ON_RESUME -> LifecycleResumeAndPauseObserver(initializer)
|
||||||
@ -74,4 +73,4 @@ fun <T : Any> LifecycleOwner.autoClear(
|
|||||||
initializer: () -> T
|
initializer: () -> T
|
||||||
): AutoClear<T> {
|
): AutoClear<T> {
|
||||||
return AutoClear(this.lifecycle, initializer, trigger)
|
return AutoClear(this.lifecycle, initializer, trigger)
|
||||||
}
|
}
|
||||||
|
@ -59,4 +59,4 @@ class AutoClearFragment<T : Any?>(
|
|||||||
|
|
||||||
fun <T : Any> Fragment.autoClear(initializer: () -> T): AutoClearFragment<T> {
|
fun <T : Any> Fragment.autoClear(initializer: () -> T): AutoClearFragment<T> {
|
||||||
return AutoClearFragment(this, initializer)
|
return AutoClearFragment(this, initializer)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,6 @@ package com.shabinder.spotiflyer.utils.autoclear
|
|||||||
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
|
||||||
interface LifecycleAutoInitializer<T>: DefaultLifecycleObserver {
|
interface LifecycleAutoInitializer<T> : DefaultLifecycleObserver {
|
||||||
var value: T?
|
var value: T?
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
||||||
|
|
||||||
class LifecycleCreateAndDestroyObserver<T: Any?>(
|
class LifecycleCreateAndDestroyObserver<T : Any?>(
|
||||||
private val initializer: (() -> T)?
|
private val initializer: (() -> T)?
|
||||||
) : LifecycleAutoInitializer<T> {
|
) : LifecycleAutoInitializer<T> {
|
||||||
|
|
||||||
@ -18,4 +18,4 @@ class LifecycleCreateAndDestroyObserver<T: Any?>(
|
|||||||
super.onDestroy(owner)
|
super.onDestroy(owner)
|
||||||
value = null
|
value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
||||||
|
|
||||||
class LifecycleResumeAndPauseObserver<T: Any?>(
|
class LifecycleResumeAndPauseObserver<T : Any?>(
|
||||||
private val initializer: (() -> T)?
|
private val initializer: (() -> T)?
|
||||||
) : LifecycleAutoInitializer<T> {
|
) : LifecycleAutoInitializer<T> {
|
||||||
|
|
||||||
@ -18,4 +18,4 @@ class LifecycleResumeAndPauseObserver<T: Any?>(
|
|||||||
super.onPause(owner)
|
super.onPause(owner)
|
||||||
value = null
|
value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package com.shabinder.spotiflyer.utils.autoclear.lifecycleobservers
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
import com.shabinder.spotiflyer.utils.autoclear.LifecycleAutoInitializer
|
||||||
|
|
||||||
class LifecycleStartAndStopObserver<T: Any?>(
|
class LifecycleStartAndStopObserver<T : Any?>(
|
||||||
private val initializer: (() -> T)?
|
private val initializer: (() -> T)?
|
||||||
) : LifecycleAutoInitializer<T> {
|
) : LifecycleAutoInitializer<T> {
|
||||||
|
|
||||||
@ -18,4 +18,4 @@ class LifecycleStartAndStopObserver<T: Any?>(
|
|||||||
super.onStop(owner)
|
super.onStop(owner)
|
||||||
value = null
|
value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,8 @@ allprojects {
|
|||||||
maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven")
|
maven(url = "https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven")
|
||||||
}
|
}
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
dependsOn(":common:data-models:generateI18n4kFiles")
|
||||||
jvmTarget = "1.8"
|
kotlinOptions { jvmTarget = "1.8" }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
project.extensions.findByType<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension>()?.let { kmpExt ->
|
project.extensions.findByType<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension>()?.let { kmpExt ->
|
||||||
|
@ -19,26 +19,14 @@ plugins {
|
|||||||
id("org.jlleitschuh.gradle.ktlint-idea")
|
id("org.jlleitschuh.gradle.ktlint-idea")
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
ktlint {
|
||||||
apply(plugin = "org.jlleitschuh.gradle.ktlint")
|
outputToConsole.set(true)
|
||||||
apply(plugin = "org.jlleitschuh.gradle.ktlint-idea")
|
ignoreFailures.set(true)
|
||||||
repositories {
|
coloredOutput.set(true)
|
||||||
// Required to download KtLint
|
verbose.set(true)
|
||||||
mavenCentral()
|
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)
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
@ -16,4 +16,4 @@ actual fun Dialog(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,4 +14,4 @@ actual fun montserratFont() = FontFamily(
|
|||||||
|
|
||||||
actual fun pristineFont() = FontFamily(
|
actual fun pristineFont() = FontFamily(
|
||||||
Font(R.font.pristine_script, FontWeight.Bold)
|
Font(R.font.pristine_script, FontWeight.Bold)
|
||||||
)
|
)
|
||||||
|
@ -7,4 +7,4 @@ expect fun Dialog(
|
|||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
)
|
)
|
||||||
|
@ -70,4 +70,4 @@ expect fun HeartIcon(): Painter
|
|||||||
expect fun DownloadImageError(modifier: Modifier)
|
expect fun DownloadImageError(modifier: Modifier)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadImageArrow(modifier: Modifier)
|
expect fun DownloadImageArrow(modifier: Modifier)
|
||||||
|
@ -35,7 +35,7 @@ import com.shabinder.common.uikit.RazorPay
|
|||||||
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
|
import com.shabinder.common.uikit.configurations.SpotiFlyerTypography
|
||||||
import com.shabinder.common.uikit.configurations.colorAccent
|
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 openAction = () -> Unit
|
||||||
internal typealias dismissAction = () -> Unit
|
internal typealias dismissAction = () -> Unit
|
||||||
private typealias snoozeAction = () -> Unit
|
private typealias snoozeAction = () -> Unit
|
||||||
@ -58,7 +58,7 @@ fun DonationDialogComponent(onDismissExtra: () -> Unit): DonationDialogCallBacks
|
|||||||
onDismissExtra()
|
onDismissExtra()
|
||||||
isDonationDialogVisible = false
|
isDonationDialogVisible = false
|
||||||
}
|
}
|
||||||
return DonationDialogCallBacks(openDonationDialog,dismissDonationDialog,snoozeDonationDialog)
|
return DonationDialogCallBacks(openDonationDialog, dismissDonationDialog, snoozeDonationDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@ -68,7 +68,7 @@ fun DonationDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSnooze: () -> Unit
|
onSnooze: () -> Unit
|
||||||
) {
|
) {
|
||||||
Dialog(isVisible,onDismiss) {
|
Dialog(isVisible, onDismiss) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
border = BorderStroke(1.dp, Color.Gray) // Gray
|
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.SpotiFlyerTypography
|
||||||
import com.shabinder.common.uikit.configurations.colorAccent
|
import com.shabinder.common.uikit.configurations.colorAccent
|
||||||
|
|
||||||
typealias ErrorInfoDialogCallBacks = Pair<openAction,dismissAction>
|
typealias ErrorInfoDialogCallBacks = Pair<openAction, dismissAction>
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorInfoDialog(error: Throwable): ErrorInfoDialogCallBacks {
|
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
|
// Donation Dialog Visibility
|
||||||
val (openDonationDialog,dismissDonationDialog,snoozeDonationDialog) = DonationDialogComponent {
|
val (openDonationDialog, dismissDonationDialog, snoozeDonationDialog) = DonationDialogComponent {
|
||||||
component.dismissDonationDialogSetOffset()
|
component.dismissDonationDialogSetOffset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,11 +184,14 @@ fun TrackCard(
|
|||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
is DownloadStatus.Failed -> {
|
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 {
|
Icon(
|
||||||
openErrorDialog()
|
Icons.Rounded.Info, Strings.downloadError(), tint = lightGray,
|
||||||
}.padding(start = 4.dp,end = 12.dp))
|
modifier = Modifier.size(42.dp).clickable {
|
||||||
|
openErrorDialog()
|
||||||
|
}.padding(start = 4.dp, end = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
DownloadImageError(
|
DownloadImageError(
|
||||||
Modifier.clickable(
|
Modifier.clickable(
|
||||||
|
@ -106,7 +106,7 @@ import com.shabinder.common.uikit.rememberScrollbarAdapter
|
|||||||
fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
|
fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
|
||||||
val model by component.model.subscribeAsState()
|
val model by component.model.subscribeAsState()
|
||||||
|
|
||||||
val (openDonationDialog,_,_) = DonationDialogComponent {
|
val (openDonationDialog, _, _) = DonationDialogComponent {
|
||||||
component.dismissDonationDialogOffset()
|
component.dismissDonationDialogOffset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ fun SearchPanel(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AboutColumn(
|
fun AboutColumn(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
analyticsEnabled:Boolean,
|
analyticsEnabled: Boolean,
|
||||||
openDonationDialog: () -> Unit,
|
openDonationDialog: () -> Unit,
|
||||||
toggleAnalytics: (enabled: Boolean) -> Unit
|
toggleAnalytics: (enabled: Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -47,7 +47,7 @@ import com.shabinder.common.uikit.configurations.colorOffWhite
|
|||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
||||||
val model by component.model.subscribeAsState()
|
val model by component.model.subscribeAsState()
|
||||||
|
|
||||||
val stateVertical = rememberScrollState(0)
|
val stateVertical = rememberScrollState(0)
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(8.dp).verticalScroll(stateVertical)) {
|
Column(Modifier.fillMaxSize().padding(8.dp).verticalScroll(stateVertical)) {
|
||||||
@ -82,7 +82,7 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
|||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(horizontal = 16.dp,vertical = 2.dp)
|
.padding(horizontal = 16.dp, vertical = 2.dp)
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = (quality == model.preferredQuality),
|
selected = (quality == model.preferredQuality),
|
||||||
@ -98,7 +98,6 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.padding(top = 12.dp))
|
Spacer(Modifier.padding(top = 12.dp))
|
||||||
@ -155,9 +154,8 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@ -165,7 +163,7 @@ fun SpotiFlyerPreferenceContent(component: SpotiFlyerPreference) {
|
|||||||
fun SettingsRow(
|
fun SettingsRow(
|
||||||
icon: Painter,
|
icon: Painter,
|
||||||
title: String,
|
title: String,
|
||||||
value:String,
|
value: String,
|
||||||
editContent: @Composable ColumnScope.(() -> Unit) -> Unit
|
editContent: @Composable ColumnScope.(() -> Unit) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -198,4 +196,4 @@ fun SettingsRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,4 @@ actual fun Dialog(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.platform.Font
|
import androidx.compose.ui.text.platform.Font
|
||||||
|
|
||||||
|
|
||||||
actual fun montserratFont() = FontFamily(
|
actual fun montserratFont() = FontFamily(
|
||||||
Font("font/montserrat_light.ttf", FontWeight.Light),
|
Font("font/montserrat_light.ttf", FontWeight.Light),
|
||||||
Font("font/montserrat_regular.ttf", FontWeight.Normal),
|
Font("font/montserrat_regular.ttf", FontWeight.Normal),
|
||||||
@ -14,4 +13,4 @@ actual fun montserratFont() = FontFamily(
|
|||||||
|
|
||||||
actual fun pristineFont() = FontFamily(
|
actual fun pristineFont() = FontFamily(
|
||||||
Font("font/pristine_script.ttf", FontWeight.Bold)
|
Font("font/pristine_script.ttf", FontWeight.Bold)
|
||||||
)
|
)
|
||||||
|
@ -29,9 +29,9 @@ val statelyVersion = "1.1.7"
|
|||||||
val statelyIsoVersion = "1.1.7-a1"
|
val statelyIsoVersion = "1.1.7-a1"
|
||||||
|
|
||||||
i18n4k {
|
i18n4k {
|
||||||
inputDirectory = "../../translations"
|
inputDirectory = "../../translations"
|
||||||
packageName = "com.shabinder.common.translations"
|
packageName = "com.shabinder.common.translations"
|
||||||
// sourceCodeLocales = listOf("en", "de", "es", "fr", "id", "pt", "ru", "uk")
|
// sourceCodeLocales = listOf("en", "de", "es", "fr", "id", "pt", "ru", "uk")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
fun <T: Any?> T?.requireNotNull() : T = requireNotNull(this)
|
fun <T : Any?> T?.requireNotNull(): T = requireNotNull(this)
|
||||||
|
@ -49,4 +49,4 @@ sealed class CorsProxy(open val url: String) {
|
|||||||
* */
|
* */
|
||||||
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
||||||
|
|
||||||
val corsApi get() = if (activePlatform is TargetPlatforms.Js) corsProxy.url else ""
|
val corsApi get() = if (activePlatform is TargetPlatforms.Js) corsProxy.url else ""
|
||||||
|
@ -2,42 +2,42 @@ package com.shabinder.common.models
|
|||||||
|
|
||||||
import com.shabinder.common.translations.Strings
|
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 FeatureNotImplementedYet(override val message: String = Strings.featureUnImplemented()) : SpotiFlyerException(message)
|
||||||
data class NoInternetException(override val message: String = Strings.checkInternetConnection()): SpotiFlyerException(message)
|
data class NoInternetException(override val message: String = Strings.checkInternetConnection()) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class MP3ConversionFailed(
|
data class MP3ConversionFailed(
|
||||||
val extraInfo:String? = null,
|
val extraInfo: String? = null,
|
||||||
override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo"
|
override val message: String = "${Strings.mp3ConverterBusy()} \nCAUSE:$extraInfo"
|
||||||
): SpotiFlyerException(message)
|
) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class UnknownReason(
|
data class UnknownReason(
|
||||||
val exception: Throwable? = null,
|
val exception: Throwable? = null,
|
||||||
override val message: String = Strings.unknownError()
|
override val message: String = Strings.unknownError()
|
||||||
): SpotiFlyerException(message)
|
) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class NoMatchFound(
|
data class NoMatchFound(
|
||||||
val trackName: String? = null,
|
val trackName: String? = null,
|
||||||
override val message: String = "$trackName : ${Strings.noMatchFound()}"
|
override val message: String = "$trackName : ${Strings.noMatchFound()}"
|
||||||
): SpotiFlyerException(message)
|
) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class YoutubeLinkNotFound(
|
data class YoutubeLinkNotFound(
|
||||||
val videoID: String? = null,
|
val videoID: String? = null,
|
||||||
override val message: String = "${Strings.noLinkFound()}: $videoID"
|
override val message: String = "${Strings.noLinkFound()}: $videoID"
|
||||||
): SpotiFlyerException(message)
|
) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class DownloadLinkFetchFailed(
|
data class DownloadLinkFetchFailed(
|
||||||
val trackName: String,
|
val trackName: String,
|
||||||
val jioSaavnError: Throwable,
|
val jioSaavnError: Throwable,
|
||||||
val ytMusicError: Throwable,
|
val ytMusicError: Throwable,
|
||||||
override val message: String = "${Strings.noLinkFound()}: $trackName," +
|
override val message: String = "${Strings.noLinkFound()}: $trackName," +
|
||||||
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
|
" \n YtMusic Error's StackTrace: ${ytMusicError.stackTraceToString()} \n " +
|
||||||
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
|
" \n JioSaavn Error's StackTrace: ${jioSaavnError.stackTraceToString()} \n "
|
||||||
): SpotiFlyerException(message)
|
) : SpotiFlyerException(message)
|
||||||
|
|
||||||
data class LinkInvalid(
|
data class LinkInvalid(
|
||||||
val link: String? = null,
|
val link: String? = null,
|
||||||
override val message: String = "${Strings.linkNotValid()}\n ${link ?: ""}"
|
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 =
|
inline fun <V, E : Throwable> Event<V, E>.unwrapError(success: (V) -> Nothing): E =
|
||||||
apply { component1()?.let(success) }.component2()!!
|
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 component1(): V? = null
|
||||||
open operator fun component2(): E? = null
|
open operator fun component2(): E? = null
|
||||||
@ -204,4 +203,4 @@ sealed class Event<out V : Any?, out E : Throwable>: ReadOnlyProperty<Any?, V> {
|
|||||||
error(ex)
|
error(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,4 +14,4 @@ inline infix fun <T, V> T.runCatching(block: T.() -> V): Event<V, Throwable> {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Event.error(e)
|
Event.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,4 @@ class Validation<out E : Throwable>(vararg resultSequence: Event<*, E>) {
|
|||||||
val failures: List<E> = resultSequence.filterIsInstance<Event.Failure<E>>().map { it.getThrowable() }
|
val failures: List<E> = resultSequence.filterIsInstance<Event.Failure<E>>().map { it.getThrowable() }
|
||||||
|
|
||||||
val hasFailure = failures.isNotEmpty()
|
val hasFailure = failures.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ infix fun <V : Any?, E : Throwable> SuspendableEvent<V, E>.or(fallback: V) = whe
|
|||||||
else -> SuspendableEvent.Success(fallback)
|
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) {
|
return when (this) {
|
||||||
is SuspendableEvent.Success -> value
|
is SuspendableEvent.Success -> value
|
||||||
is SuspendableEvent.Failure -> fallback(error)
|
is SuspendableEvent.Failure -> fallback(error)
|
||||||
@ -93,7 +93,6 @@ suspend inline fun <V : Any?, U : Any> SuspendableEvent<V, *>.fanout(
|
|||||||
): SuspendableEvent<Pair<V, U>, *> =
|
): SuspendableEvent<Pair<V, U>, *> =
|
||||||
flatMap { outer -> other().map { outer to it } }
|
flatMap { outer -> other().map { outer to it } }
|
||||||
|
|
||||||
|
|
||||||
suspend fun <V : Any?, E : Throwable> List<SuspendableEvent<V, E>>.lift(): SuspendableEvent<List<V>, E> = fold(
|
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>
|
SuspendableEvent.Success<MutableList<V>, E>(mutableListOf<V>()) as SuspendableEvent<MutableList<V>, E>
|
||||||
) { acc, result ->
|
) { 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 component1(): V?
|
||||||
abstract operator fun component2(): E?
|
abstract operator fun component2(): E?
|
||||||
@ -156,7 +155,7 @@ sealed class SuspendableEvent<out V : Any?, out E : Throwable>: ReadOnlyProperty
|
|||||||
// Factory methods
|
// Factory methods
|
||||||
fun <E : Throwable> error(ex: E) = Failure<Nothing, E>(ex)
|
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())
|
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
|
crossinline block: suspend () -> V
|
||||||
): SuspendableEvent<V, Throwable> = of(block)
|
): 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 failures: List<E> = resultSequence.filterIsInstance<SuspendableEvent.Failure<*, E>>().map { it.getThrowable() }
|
||||||
|
|
||||||
val hasFailure = failures.isNotEmpty()
|
val hasFailure = failures.isNotEmpty()
|
||||||
|
}
|
||||||
}
|
|
||||||
|
@ -28,9 +28,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.IOException
|
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
|||||||
album = track.albumName
|
album = track.albumName
|
||||||
year = track.year
|
year = track.year
|
||||||
comment = "Genres:${track.comment}"
|
comment = "Genres:${track.comment}"
|
||||||
if(track.trackNumber != null)
|
if (track.trackNumber != null)
|
||||||
this.track = track.trackNumber.toString()
|
this.track = track.trackNumber.toString()
|
||||||
}
|
}
|
||||||
this.id3v1Tag = id3v1Tag
|
this.id3v1Tag = id3v1Tag
|
||||||
@ -60,7 +60,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
|||||||
comment = track.comment
|
comment = track.comment
|
||||||
lyrics = track.lyrics ?: ""
|
lyrics = track.lyrics ?: ""
|
||||||
url = track.trackUrl
|
url = track.trackUrl
|
||||||
if(track.trackNumber != null)
|
if (track.trackNumber != null)
|
||||||
this.track = track.trackNumber.toString()
|
this.track = track.trackNumber.toString()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
@ -59,7 +59,7 @@ fun Dir.createDirectories() {
|
|||||||
createDirectory(defaultDir() + "Albums/")
|
createDirectory(defaultDir() + "Albums/")
|
||||||
createDirectory(defaultDir() + "Playlists/")
|
createDirectory(defaultDir() + "Playlists/")
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
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 =
|
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 authenticateSpotifyClient() = spotifyProvider.authenticateSpotifyClient()
|
||||||
|
|
||||||
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult,Throwable> {
|
suspend fun query(link: String): SuspendableEvent<PlatformQueryResult, Throwable> {
|
||||||
val result = when {
|
val result = when {
|
||||||
// SPOTIFY
|
// SPOTIFY
|
||||||
link.contains("spotify", true) ->
|
link.contains("spotify", true) ->
|
||||||
@ -94,17 +94,17 @@ class FetchPlatformQueryResult(
|
|||||||
suspend fun findMp3DownloadLink(
|
suspend fun findMp3DownloadLink(
|
||||||
track: TrackDetails,
|
track: TrackDetails,
|
||||||
preferredQuality: AudioQuality = preferenceManager.audioQuality
|
preferredQuality: AudioQuality = preferenceManager.audioQuality
|
||||||
): SuspendableEvent<String,Throwable> =
|
): SuspendableEvent<String, Throwable> =
|
||||||
if (track.videoID != null) {
|
if (track.videoID != null) {
|
||||||
// We Already have VideoID
|
// We Already have VideoID
|
||||||
when (track.source) {
|
when (track.source) {
|
||||||
Source.JioSaavn -> {
|
Source.JioSaavn -> {
|
||||||
saavnProvider.getSongFromID(track.videoID.requireNotNull()).flatMap { song ->
|
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 -> {
|
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" }
|
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 ->
|
youtubeProvider.ytDownloader.getVideo(track.videoID!!).get()?.url?.let { m4aLink ->
|
||||||
audioToMp3.convertToMp3(m4aLink)
|
audioToMp3.convertToMp3(m4aLink)
|
||||||
@ -113,17 +113,17 @@ class FetchPlatformQueryResult(
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
/*We should never reach here for now*/
|
/*We should never reach here for now*/
|
||||||
findMp3Link(track,preferredQuality)
|
findMp3Link(track, preferredQuality)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
findMp3Link(track,preferredQuality)
|
findMp3Link(track, preferredQuality)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun findMp3Link(
|
private suspend fun findMp3Link(
|
||||||
track: TrackDetails,
|
track: TrackDetails,
|
||||||
preferredQuality: AudioQuality
|
preferredQuality: AudioQuality
|
||||||
):SuspendableEvent<String,Throwable> {
|
): SuspendableEvent<String, Throwable> {
|
||||||
// Try Fetching Track from Jio Saavn
|
// Try Fetching Track from Jio Saavn
|
||||||
return saavnProvider.findMp3SongDownloadURL(
|
return saavnProvider.findMp3SongDownloadURL(
|
||||||
trackName = track.title,
|
trackName = track.title,
|
||||||
@ -132,11 +132,11 @@ class FetchPlatformQueryResult(
|
|||||||
).flatMapError { saavnError ->
|
).flatMapError { saavnError ->
|
||||||
logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
|
logger.e { "Fetching From Saavn Failed: \n${saavnError.stackTraceToString()}" }
|
||||||
// Saavn Failed, Lets Try Fetching Now From Youtube Music
|
// 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
|
// If Both Failed Bubble the Exception Up with both StackTraces
|
||||||
SuspendableEvent.error(
|
SuspendableEvent.error(
|
||||||
SpotiFlyerException.DownloadLinkFetchFailed(
|
SpotiFlyerException.DownloadLinkFetchFailed(
|
||||||
trackName = track.title,
|
trackName = track.title,
|
||||||
ytMusicError = ytMusicError,
|
ytMusicError = ytMusicError,
|
||||||
jioSaavnError = saavnError
|
jioSaavnError = saavnError
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ package com.shabinder.common.di.preference
|
|||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.shabinder.common.models.AudioQuality
|
import com.shabinder.common.models.AudioQuality
|
||||||
|
|
||||||
class PreferenceManager(settings: Settings): Settings by settings {
|
class PreferenceManager(settings: Settings) : Settings by settings {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DIR_KEY = "downloadDir"
|
const val DIR_KEY = "downloadDir"
|
||||||
@ -17,14 +17,13 @@ class PreferenceManager(settings: Settings): Settings by settings {
|
|||||||
val isAnalyticsEnabled get() = getBooleanOrNull(ANALYTICS_KEY) ?: false
|
val isAnalyticsEnabled get() = getBooleanOrNull(ANALYTICS_KEY) ?: false
|
||||||
fun toggleAnalytics(enabled: Boolean) = putBoolean(ANALYTICS_KEY, enabled)
|
fun toggleAnalytics(enabled: Boolean) = putBoolean(ANALYTICS_KEY, enabled)
|
||||||
|
|
||||||
|
|
||||||
/* DOWNLOAD DIRECTORY */
|
/* DOWNLOAD DIRECTORY */
|
||||||
val downloadDir get() = getStringOrNull(DIR_KEY)
|
val downloadDir get() = getStringOrNull(DIR_KEY)
|
||||||
fun setDownloadDirectory(newBasePath: String) = putString(DIR_KEY, newBasePath)
|
fun setDownloadDirectory(newBasePath: String) = putString(DIR_KEY, newBasePath)
|
||||||
|
|
||||||
/* Preferred Audio Quality */
|
/* Preferred Audio Quality */
|
||||||
val audioQuality get() = AudioQuality.getQuality(getStringOrNull(PREFERRED_AUDIO_QUALITY) ?: "320")
|
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 */
|
/* OFFSET FOR WHEN TO ASK FOR SUPPORT */
|
||||||
val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
|
val getDonationOffset: Int get() = (getIntOrNull(DONATION_INTERVAL) ?: 3).also {
|
||||||
@ -33,8 +32,7 @@ class PreferenceManager(settings: Settings): Settings by settings {
|
|||||||
}
|
}
|
||||||
fun setDonationOffset(offset: Int = 5) = putInt(DONATION_INTERVAL, offset)
|
fun setDonationOffset(offset: Int = 5) = putInt(DONATION_INTERVAL, offset)
|
||||||
|
|
||||||
|
|
||||||
/* TO CHECK IF THIS IS APP's FIRST LAUNCH */
|
/* TO CHECK IF THIS IS APP's FIRST LAUNCH */
|
||||||
val isFirstLaunch get() = getBooleanOrNull(FIRST_LAUNCH) ?: true
|
val isFirstLaunch get() = getBooleanOrNull(FIRST_LAUNCH) ?: true
|
||||||
fun firstLaunchDone() = putBoolean(FIRST_LAUNCH, false)
|
fun firstLaunchDone() = putBoolean(FIRST_LAUNCH, false)
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ class GaanaProvider(
|
|||||||
|
|
||||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
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
|
// Link Schema: https://gaana.com/type/link
|
||||||
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
||||||
|
|
||||||
|
@ -13,4 +13,4 @@ fun providersModule() = module {
|
|||||||
single { YoutubeMp3(get(), get()) }
|
single { YoutubeMp3(get(), get()) }
|
||||||
single { YoutubeMusic(get(), get(), get(), get(), get()) }
|
single { YoutubeMusic(get(), get(), get(), get(), get()) }
|
||||||
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class SaavnProvider(
|
|||||||
).apply {
|
).apply {
|
||||||
val pageLink = fullLink.substringAfter("saavn.com/").substringBefore("?")
|
val pageLink = fullLink.substringAfter("saavn.com/").substringBefore("?")
|
||||||
when {
|
when {
|
||||||
pageLink.contains("/song/",true) -> {
|
pageLink.contains("/song/", true) -> {
|
||||||
getSong(fullLink).value.let {
|
getSong(fullLink).value.let {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
@ -42,7 +42,7 @@ class SaavnProvider(
|
|||||||
coverUrl = it.image.replace("http:", "https:")
|
coverUrl = it.image.replace("http:", "https:")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageLink.contains("/album/",true) -> {
|
pageLink.contains("/album/", true) -> {
|
||||||
getAlbum(fullLink).value.let {
|
getAlbum(fullLink).value.let {
|
||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = removeIllegalChars(it.title)
|
subFolder = removeIllegalChars(it.title)
|
||||||
@ -51,7 +51,7 @@ class SaavnProvider(
|
|||||||
coverUrl = it.image.replace("http:", "https:")
|
coverUrl = it.image.replace("http:", "https:")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageLink.contains("/featured/",true) -> { // Playlist
|
pageLink.contains("/featured/", true) -> { // Playlist
|
||||||
getPlaylist(fullLink).value.let {
|
getPlaylist(fullLink).value.let {
|
||||||
folderType = "Playlists"
|
folderType = "Playlists"
|
||||||
subFolder = removeIllegalChars(it.listname)
|
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 com.shabinder.common.models.event.coroutines.map
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
|
||||||
interface YoutubeMp3: Yt1sMp3 {
|
interface YoutubeMp3 : Yt1sMp3 {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
operator fun invoke(
|
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
|
corsApi + it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ class YoutubeMusic constructor(
|
|||||||
): SuspendableEvent<String, Throwable> {
|
): SuspendableEvent<String, Throwable> {
|
||||||
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
|
return getYTIDBestMatch(trackDetails).flatMap { videoID ->
|
||||||
// As YT compress Audio hence there is no benefit of quality for more than 192
|
// 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
|
// 1 Try getting Link from Yt1s
|
||||||
youtubeMp3.getMp3DownloadLink(videoID, optimalQuality).flatMapError {
|
youtubeMp3.getMp3DownloadLink(videoID, optimalQuality).flatMapError {
|
||||||
// 2 if Yt1s failed , Extract Manually
|
// 2 if Yt1s failed , Extract Manually
|
||||||
@ -79,7 +79,7 @@ class YoutubeMusic constructor(
|
|||||||
|
|
||||||
private suspend fun getYTIDBestMatch(
|
private suspend fun getYTIDBestMatch(
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
):SuspendableEvent<String,Throwable> =
|
): SuspendableEvent<String, Throwable> =
|
||||||
getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList ->
|
getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}").map { matchList ->
|
||||||
sortByBestMatch(
|
sortByBestMatch(
|
||||||
matchList,
|
matchList,
|
||||||
@ -89,7 +89,7 @@ class YoutubeMusic constructor(
|
|||||||
).keys.firstOrNull() ?: throw SpotiFlyerException.NoMatchFound(trackDetails.title)
|
).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 ->
|
getYoutubeMusicResponse(query).map { youtubeResponseData ->
|
||||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
val responseObj = Json.parseToJsonElement(youtubeResponseData)
|
val responseObj = Json.parseToJsonElement(youtubeResponseData)
|
||||||
@ -233,9 +233,9 @@ class YoutubeMusic constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// logger.d {youtubeTracks.joinToString("\n")}
|
// logger.d {youtubeTracks.joinToString("\n")}
|
||||||
youtubeTracks
|
youtubeTracks
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortByBestMatch(
|
private fun sortByBestMatch(
|
||||||
ytTracks: List<YoutubeTrack>,
|
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") {
|
httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
headers {
|
headers {
|
||||||
|
@ -51,7 +51,7 @@ class YoutubeProvider(
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
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://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if (link.contains("playlist", true) || link.contains("list", true)) {
|
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
@ -86,7 +86,7 @@ class YoutubeProvider(
|
|||||||
|
|
||||||
private suspend fun getYTPlaylist(
|
private suspend fun getYTPlaylist(
|
||||||
searchId: String
|
searchId: String
|
||||||
): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
|
): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
|
||||||
PlatformQueryResult(
|
PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
@ -102,7 +102,7 @@ class YoutubeProvider(
|
|||||||
val videos = playlist.videos
|
val videos = playlist.videos
|
||||||
|
|
||||||
coverUrl = "https://i.ytimg.com/vi/${
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
videos.firstOrNull()?.videoId
|
videos.firstOrNull()?.videoId
|
||||||
}/hqdefault.jpg"
|
}/hqdefault.jpg"
|
||||||
title = name
|
title = name
|
||||||
|
|
||||||
@ -116,11 +116,11 @@ class YoutubeProvider(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = it.title ?: "N/A",
|
itemName = it.title ?: "N/A",
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
dir.defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
@ -137,7 +137,7 @@ class YoutubeProvider(
|
|||||||
@Suppress("DefaultLocale")
|
@Suppress("DefaultLocale")
|
||||||
private suspend fun getYTTrack(
|
private suspend fun getYTTrack(
|
||||||
searchId: String,
|
searchId: String,
|
||||||
): SuspendableEvent<PlatformQueryResult,Throwable> = SuspendableEvent {
|
): SuspendableEvent<PlatformQueryResult, Throwable> = SuspendableEvent {
|
||||||
PlatformQueryResult(
|
PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
@ -162,11 +162,11 @@ class YoutubeProvider(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = dir.defaultDir()
|
defaultDir = dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
|
@ -32,10 +32,10 @@ interface AudioToMp3 {
|
|||||||
suspend fun convertToMp3(
|
suspend fun convertToMp3(
|
||||||
URL: String,
|
URL: String,
|
||||||
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
|
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
|
// Active Host ex - https://hostveryfast.onlineconverter.com/file/send
|
||||||
// Convert Job Request ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
|
// 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
|
// (jobStatus.contains("d")) == COMPLETION
|
||||||
var jobStatus: String
|
var jobStatus: String
|
||||||
@ -48,7 +48,7 @@ interface AudioToMp3 {
|
|||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
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
|
// No Need to Retry, Host/Converter is Busy
|
||||||
throw SpotiFlyerException.MP3ConversionFailed(e.message)
|
throw SpotiFlyerException.MP3ConversionFailed(e.message)
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ interface AudioToMp3 {
|
|||||||
private suspend fun convertRequest(
|
private suspend fun convertRequest(
|
||||||
URL: String,
|
URL: String,
|
||||||
audioQuality: AudioQuality = AudioQuality.KBPS160,
|
audioQuality: AudioQuality = AudioQuality.KBPS160,
|
||||||
): SuspendableEvent<Pair<String,String>,Throwable> = SuspendableEvent {
|
): SuspendableEvent<Pair<String, String>, Throwable> = SuspendableEvent {
|
||||||
val activeHost by getHost()
|
val activeHost by getHost()
|
||||||
val convertJob = client.submitFormWithBinaryData<String>(
|
val convertJob = client.submitFormWithBinaryData<String>(
|
||||||
url = activeHost,
|
url = activeHost,
|
||||||
@ -104,17 +104,17 @@ interface AudioToMp3 {
|
|||||||
}.execute()
|
}.execute()
|
||||||
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
|
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
|
||||||
|
|
||||||
Pair(activeHost,convertJob)
|
Pair(activeHost, convertJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active Host free to process conversion
|
// Active Host free to process conversion
|
||||||
// ex - https://hostveryfast.onlineconverter.com/file/send
|
// 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") {
|
client.get<String>("https://www.onlineconverter.com/get/host") {
|
||||||
headers {
|
headers {
|
||||||
header("Host", "www.onlineconverter.com")
|
header("Host", "www.onlineconverter.com")
|
||||||
}
|
}
|
||||||
}//.also { logger.i("Active Host") { it } }
|
} // .also { logger.i("Active Host") { it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract full Domain from URL
|
// Extract full Domain from URL
|
||||||
|
@ -25,7 +25,6 @@ import com.shabinder.common.models.gaana.GaanaSong
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
|
|
||||||
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
|
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
|
||||||
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
|
private val BASE_URL get() = "${corsApi}https://api.gaana.com"
|
||||||
|
|
||||||
|
@ -40,12 +40,12 @@ interface JioSaavnRequests {
|
|||||||
trackName: String,
|
trackName: String,
|
||||||
trackArtists: List<String>,
|
trackArtists: List<String>,
|
||||||
preferredQuality: AudioQuality
|
preferredQuality: AudioQuality
|
||||||
): SuspendableEvent<String,Throwable> = searchForSong(trackName).map { songs ->
|
): SuspendableEvent<String, Throwable> = searchForSong(trackName).map { songs ->
|
||||||
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
|
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
|
||||||
|
|
||||||
val m4aLink: String by getSongFromID(bestMatches.keys.first()).map { song ->
|
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
|
val optimalQuality = if (song.is320Kbps && ((preferredQuality.kbps.toIntOrNull() ?: 0) > 160)) AudioQuality.KBPS320 else AudioQuality.KBPS160
|
||||||
song.media_url.requireNotNull().replaceAfterLast("_","${optimalQuality.kbps}.mp4")
|
song.media_url.requireNotNull().replaceAfterLast("_", "${optimalQuality.kbps}.mp4")
|
||||||
}
|
}
|
||||||
|
|
||||||
val mp3Link by audioToMp3.convertToMp3(m4aLink)
|
val mp3Link by audioToMp3.convertToMp3(m4aLink)
|
||||||
@ -56,7 +56,7 @@ interface JioSaavnRequests {
|
|||||||
suspend fun searchForSong(
|
suspend fun searchForSong(
|
||||||
query: String,
|
query: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): SuspendableEvent<List<SaavnSearchResult>,Throwable> = SuspendableEvent {
|
): SuspendableEvent<List<SaavnSearchResult>, Throwable> = SuspendableEvent {
|
||||||
|
|
||||||
val searchURL = search_base_url + query
|
val searchURL = search_base_url + query
|
||||||
val results = mutableListOf<SaavnSearchResult>()
|
val results = mutableListOf<SaavnSearchResult>()
|
||||||
@ -67,12 +67,12 @@ interface JioSaavnRequests {
|
|||||||
(it as JsonObject).formatData().let { jsonObject ->
|
(it as JsonObject).formatData().let { jsonObject ->
|
||||||
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results
|
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)
|
(Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
|
||||||
.getString("lyrics").requireNotNull()
|
.getString("lyrics").requireNotNull()
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getSong(
|
suspend fun getSong(
|
||||||
URL: String,
|
URL: String,
|
||||||
fetchLyrics: Boolean = false
|
fetchLyrics: Boolean = false
|
||||||
): SuspendableEvent<SaavnSong,Throwable> = SuspendableEvent {
|
): SuspendableEvent<SaavnSong, Throwable> = SuspendableEvent {
|
||||||
val id = getSongID(URL)
|
val id = getSongID(URL)
|
||||||
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
|
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
|
||||||
.formatData(fetchLyrics)
|
.formatData(fetchLyrics)
|
||||||
@ -91,7 +91,7 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getSongFromID(
|
suspend fun getSongFromID(
|
||||||
ID: String,
|
ID: String,
|
||||||
fetchLyrics: Boolean = false
|
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)
|
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
|
||||||
.formatData(fetchLyrics)
|
.formatData(fetchLyrics)
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getPlaylist(
|
suspend fun getPlaylist(
|
||||||
URL: String,
|
URL: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): SuspendableEvent<SaavnPlaylist,Throwable> = SuspendableEvent {
|
): SuspendableEvent<SaavnPlaylist, Throwable> = SuspendableEvent {
|
||||||
globalJson.decodeFromJsonElement(
|
globalJson.decodeFromJsonElement(
|
||||||
SaavnPlaylist.serializer(),
|
SaavnPlaylist.serializer(),
|
||||||
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject)
|
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL).value)) as JsonObject)
|
||||||
@ -122,7 +122,7 @@ interface JioSaavnRequests {
|
|||||||
|
|
||||||
private suspend fun getPlaylistID(
|
private suspend fun getPlaylistID(
|
||||||
URL: String
|
URL: String
|
||||||
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
): SuspendableEvent<String, Throwable> = SuspendableEvent {
|
||||||
val res = httpClient.get<String>(URL)
|
val res = httpClient.get<String>(URL)
|
||||||
try {
|
try {
|
||||||
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
||||||
@ -134,7 +134,7 @@ interface JioSaavnRequests {
|
|||||||
suspend fun getAlbum(
|
suspend fun getAlbum(
|
||||||
URL: String,
|
URL: String,
|
||||||
includeLyrics: Boolean = false
|
includeLyrics: Boolean = false
|
||||||
): SuspendableEvent<SaavnAlbum,Throwable> = SuspendableEvent {
|
): SuspendableEvent<SaavnAlbum, Throwable> = SuspendableEvent {
|
||||||
globalJson.decodeFromJsonElement(
|
globalJson.decodeFromJsonElement(
|
||||||
SaavnAlbum.serializer(),
|
SaavnAlbum.serializer(),
|
||||||
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject)
|
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL).value)) as JsonObject)
|
||||||
@ -144,7 +144,7 @@ interface JioSaavnRequests {
|
|||||||
|
|
||||||
private suspend fun getAlbumID(
|
private suspend fun getAlbumID(
|
||||||
URL: String
|
URL: String
|
||||||
): SuspendableEvent<String,Throwable> = SuspendableEvent {
|
): SuspendableEvent<String, Throwable> = SuspendableEvent {
|
||||||
val res = httpClient.get<String>(URL)
|
val res = httpClient.get<String>(URL)
|
||||||
try {
|
try {
|
||||||
res.split("\"album_id\":\"")[1].split('"')[0]
|
res.split("\"album_id\":\"")[1].split('"')[0]
|
||||||
|
@ -31,7 +31,7 @@ import io.ktor.client.request.forms.*
|
|||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlin.native.concurrent.SharedImmutable
|
import kotlin.native.concurrent.SharedImmutable
|
||||||
|
|
||||||
suspend fun authenticateSpotify(): SuspendableEvent<TokenData,Throwable> = SuspendableEvent {
|
suspend fun authenticateSpotify(): SuspendableEvent<TokenData, Throwable> = SuspendableEvent {
|
||||||
if (methods.value.isInternetAvailable) {
|
if (methods.value.isInternetAvailable) {
|
||||||
spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
|
spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
|
||||||
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
|
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
|
||||||
|
@ -43,7 +43,7 @@ interface Yt1sMp3 {
|
|||||||
/*
|
/*
|
||||||
* Downloadable Mp3 Link for YT videoID.
|
* 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 {
|
getConvertedMp3Link(videoID, key).map {
|
||||||
it["dlink"].requireNotNull()
|
it["dlink"].requireNotNull()
|
||||||
.jsonPrimitive.content.replace("\"", "")
|
.jsonPrimitive.content.replace("\"", "")
|
||||||
@ -54,7 +54,7 @@ interface Yt1sMp3 {
|
|||||||
* POST:https://yt1s.com/api/ajaxSearch/index
|
* POST:https://yt1s.com/api/ajaxSearch/index
|
||||||
* Body Form= q:yt video link ,vt:format=mp3
|
* 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") {
|
val response: JsonObject = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
|
||||||
body = FormDataContent(
|
body = FormDataContent(
|
||||||
Parameters.build {
|
Parameters.build {
|
||||||
@ -67,7 +67,7 @@ interface Yt1sMp3 {
|
|||||||
val mp3Keys = response.getJsonObject("links")
|
val mp3Keys = response.getJsonObject("links")
|
||||||
.getJsonObject("mp3")
|
.getJsonObject("mp3")
|
||||||
|
|
||||||
val requestedKBPS = when(quality) {
|
val requestedKBPS = when (quality) {
|
||||||
AudioQuality.KBPS128 -> "mp3128"
|
AudioQuality.KBPS128 -> "mp3128"
|
||||||
else -> quality.kbps
|
else -> quality.kbps
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ interface Yt1sMp3 {
|
|||||||
specificQualityKey?.get("k").requireNotNull().jsonPrimitive.content
|
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") {
|
httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
|
||||||
body = FormDataContent(
|
body = FormDataContent(
|
||||||
Parameters.build {
|
Parameters.build {
|
||||||
|
@ -165,7 +165,7 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<
|
|||||||
componentContext = componentContext,
|
componentContext = componentContext,
|
||||||
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
|
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {
|
||||||
override val mainOutput: Consumer<SpotiFlyerMain.Output> = output
|
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"))
|
||||||
implementation(project(":common:list"))
|
implementation(project(":common:list"))
|
||||||
|
|
||||||
|
|
||||||
// Decompose
|
// Decompose
|
||||||
implementation(Decompose.decompose)
|
implementation(Decompose.decompose)
|
||||||
implementation(Decompose.extensionsCompose)
|
implementation(Decompose.extensionsCompose)
|
||||||
|
@ -6,4 +6,4 @@ val String.byProperty: String get() = System.getenv(this)
|
|||||||
val String.byOptionalProperty: String? get() = System.getenv(this)
|
val String.byOptionalProperty: String? get() = System.getenv(this)
|
||||||
|
|
||||||
fun debug(message: String) = println("\n::debug::$message")
|
fun debug(message: String) = println("\n::debug::$message")
|
||||||
fun debug(tag: String, message: String) = println("\n::debug::$tag:\n$message")
|
fun debug(tag: String, message: String) = println("\n::debug::$tag:\n$message")
|
||||||
|
@ -20,6 +20,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("multiplatform")
|
kotlin("multiplatform")
|
||||||
id("org.jetbrains.compose")
|
id("org.jetbrains.compose")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.shabinder"
|
group = "com.shabinder"
|
||||||
@ -88,4 +89,4 @@ compose.desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,9 +56,8 @@ import java.net.URI
|
|||||||
import javax.swing.JFileChooser
|
import javax.swing.JFileChooser
|
||||||
import javax.swing.JFileChooser.APPROVE_OPTION
|
import javax.swing.JFileChooser.APPROVE_OPTION
|
||||||
|
|
||||||
|
|
||||||
private val koin = initKoin(enableNetworkLogs = true).koin
|
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 {
|
private val tracker: PiwikTracker by lazy {
|
||||||
PiwikTracker("https://matomo.spotiflyer.ml/matomo.php")
|
PiwikTracker("https://matomo.spotiflyer.ml/matomo.php")
|
||||||
}
|
}
|
||||||
@ -68,7 +67,7 @@ fun main() {
|
|||||||
val lifecycle = LifecycleRegistry()
|
val lifecycle = LifecycleRegistry()
|
||||||
lifecycle.resume()
|
lifecycle.resume()
|
||||||
|
|
||||||
Window("SpotiFlyer",size = IntSize(450,800)) {
|
Window("SpotiFlyer", size = IntSize(450, 800)) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
@ -80,7 +79,7 @@ fun main() {
|
|||||||
shapes = SpotiFlyerShapes
|
shapes = SpotiFlyerShapes
|
||||||
) {
|
) {
|
||||||
val root: SpotiFlyerRoot = SpotiFlyerRootContent(rememberRootComponent(factory = ::spotiFlyerRoot))
|
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 database: Database? = dir.db
|
||||||
override val preferenceManager: PreferenceManager = koin.get()
|
override val preferenceManager: PreferenceManager = koin.get()
|
||||||
override val downloadProgressFlow = DownloadProgressFlow
|
override val downloadProgressFlow = DownloadProgressFlow
|
||||||
override val actions: Actions = object: Actions {
|
override val actions: Actions = object : Actions {
|
||||||
override val platformActions = object : PlatformActions {}
|
override val platformActions = object : PlatformActions {}
|
||||||
|
|
||||||
override fun showPopUpMessage(string: String, long: Boolean) {
|
override fun showPopUpMessage(string: String, long: Boolean) {
|
||||||
if(::showToast.isInitialized){
|
if (::showToast.isInitialized) {
|
||||||
showToast(string)
|
showToast(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,7 +113,7 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
|
|||||||
when (fileChooser.showOpenDialog(AppManager.focusedWindow?.window)) {
|
when (fileChooser.showOpenDialog(AppManager.focusedWindow?.window)) {
|
||||||
APPROVE_OPTION -> {
|
APPROVE_OPTION -> {
|
||||||
val directory = fileChooser.selectedFile
|
val directory = fileChooser.selectedFile
|
||||||
if(directory.canWrite()){
|
if (directory.canWrite()) {
|
||||||
preferenceManager.setDownloadDirectory(directory.absolutePath)
|
preferenceManager.setDownloadDirectory(directory.absolutePath)
|
||||||
callBack(directory.absolutePath)
|
callBack(directory.absolutePath)
|
||||||
showPopUpMessage("${Strings.setDownloadDirectory()} \n${dir.defaultDir()}")
|
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() {
|
override fun giveDonation() {
|
||||||
openLink("https://razorpay.com/payment-button/pl_GnKuuDBdBu0ank/view/?utm_source=payment_button&utm_medium=button&utm_campaign=payment_button")
|
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)
|
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)) {
|
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(URI(link))
|
Desktop.getDesktop().browse(URI(link))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
|
override fun writeMp3Tags(trackDetails: TrackDetails) { /*IMPLEMENTED*/ }
|
||||||
|
|
||||||
override val isInternetAvailable: Boolean
|
override val isInternetAvailable: Boolean
|
||||||
get() = runBlocking {
|
get() = runBlocking {
|
||||||
isInternetAccessible()
|
isInternetAccessible()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override val analytics = object: SpotiFlyerRoot.Analytics {
|
override val analytics = object : SpotiFlyerRoot.Analytics {
|
||||||
override fun appLaunchEvent() {
|
override fun appLaunchEvent() {
|
||||||
if(preferenceManager.isFirstLaunch) {
|
if (preferenceManager.isFirstLaunch) {
|
||||||
// Enable Analytics on First Launch
|
// Enable Analytics on First Launch
|
||||||
preferenceManager.toggleAnalytics(true)
|
preferenceManager.toggleAnalytics(true)
|
||||||
preferenceManager.firstLaunchDone()
|
preferenceManager.firstLaunchDone()
|
||||||
|
@ -4,9 +4,8 @@ import org.piwik.java.tracking.PiwikRequest
|
|||||||
import org.piwik.java.tracking.PiwikTracker
|
import org.piwik.java.tracking.PiwikTracker
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
|
||||||
fun PiwikTracker.trackAsync(
|
fun PiwikTracker.trackAsync(
|
||||||
baseURL:String = "https://com.shabinder.spotiflyer/",
|
baseURL: String = "https://com.shabinder.spotiflyer/",
|
||||||
requestBuilder: PiwikRequest.() -> Unit = {}
|
requestBuilder: PiwikRequest.() -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val req = PiwikRequest(
|
val req = PiwikRequest(
|
||||||
@ -18,7 +17,7 @@ fun PiwikTracker.trackAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun PiwikTracker.trackScreenAsync(
|
fun PiwikTracker.trackScreenAsync(
|
||||||
screenAddress:String,
|
screenAddress: String,
|
||||||
requestBuilder: PiwikRequest.() -> Unit = {}
|
requestBuilder: PiwikRequest.() -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val req = PiwikRequest(
|
val req = PiwikRequest(
|
||||||
@ -27,4 +26,4 @@ fun PiwikTracker.trackScreenAsync(
|
|||||||
).apply { requestBuilder() }
|
).apply { requestBuilder() }
|
||||||
// Send Request
|
// Send Request
|
||||||
sendRequestAsync(req)
|
sendRequestAsync(req)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# 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.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# 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
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
@ -7,10 +7,10 @@ fun getUpdatedContent(
|
|||||||
oldContent: String,
|
oldContent: String,
|
||||||
newInsertionText: String,
|
newInsertionText: String,
|
||||||
tagName: String
|
tagName: String
|
||||||
): String{
|
): String {
|
||||||
return getReplaceableRegex(tagName).replace(
|
return getReplaceableRegex(tagName).replace(
|
||||||
oldContent,
|
oldContent,
|
||||||
getReplacementText(tagName,newInsertionText)
|
getReplacementText(tagName, newInsertionText)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,5 +26,5 @@ private fun getReplacementText(
|
|||||||
${Common.START_SECTION(tagName)}
|
${Common.START_SECTION(tagName)}
|
||||||
$newInsertionText
|
$newInsertionText
|
||||||
${Common.END_SECTION(tagName)}
|
${Common.END_SECTION(tagName)}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
@ -13,4 +13,4 @@ fun getTodayDate(): String {
|
|||||||
val year: Int = c.get(Calendar.YEAR)
|
val year: Int = c.get(Calendar.YEAR)
|
||||||
val date: Int = c.get(Calendar.DATE)
|
val date: Int = c.get(Calendar.DATE)
|
||||||
return " $date $month, $year"
|
return " $date $month, $year"
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ internal object GithubService {
|
|||||||
|
|
||||||
private const val baseURL = Common.GITHUB_API
|
private const val baseURL = Common.GITHUB_API
|
||||||
|
|
||||||
|
|
||||||
suspend fun getGithubRepoReleasesInfo(
|
suspend fun getGithubRepoReleasesInfo(
|
||||||
ownerName: String,
|
ownerName: String,
|
||||||
repoName: String,
|
repoName: String,
|
||||||
|
@ -25,8 +25,8 @@ fun main(args: Array<String>) {
|
|||||||
updatedGithubContent,
|
updatedGithubContent,
|
||||||
secrets
|
secrets
|
||||||
)
|
)
|
||||||
} catch (e:Exception) {
|
} catch (e: Exception) {
|
||||||
debug("Analytics Image Updation Failed",e.message.toString())
|
debug("Analytics Image Updation Failed", e.message.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TASK -> Update Total Downloads Card
|
// TASK -> Update Total Downloads Card
|
||||||
@ -35,8 +35,8 @@ fun main(args: Array<String>) {
|
|||||||
updatedGithubContent,
|
updatedGithubContent,
|
||||||
secrets.copy(tagName = "DCI")
|
secrets.copy(tagName = "DCI")
|
||||||
)
|
)
|
||||||
} catch (e:Exception) {
|
} catch (e: Exception) {
|
||||||
debug("Download Card Updation Failed",e.message.toString())
|
debug("Download Card Updation Failed", e.message.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write New Updated README.md
|
// Write New Updated README.md
|
||||||
|
@ -17,4 +17,4 @@ data class Asset(
|
|||||||
val updated_at: String,
|
val updated_at: String,
|
||||||
val uploader: Uploader,
|
val uploader: Uploader,
|
||||||
val url: String
|
val url: String
|
||||||
)
|
)
|
||||||
|
@ -22,4 +22,4 @@ data class Author(
|
|||||||
val subscriptions_url: String,
|
val subscriptions_url: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val url: String
|
val url: String
|
||||||
)
|
)
|
||||||
|
@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable
|
|||||||
data class GithubFileContent(
|
data class GithubFileContent(
|
||||||
val decryptedContent: String,
|
val decryptedContent: String,
|
||||||
val sha: String
|
val sha: String
|
||||||
)
|
)
|
||||||
|
@ -23,4 +23,4 @@ data class GithubReleaseInfoItem(
|
|||||||
val upload_url: String,
|
val upload_url: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val zipball_url: String
|
val zipball_url: String
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package models.github
|
package models.github
|
||||||
|
|
||||||
typealias GithubReleasesInfo = ArrayList<GithubReleaseInfoItem>
|
typealias GithubReleasesInfo = ArrayList<GithubReleaseInfoItem>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package models.github
|
package models.github
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonNames
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Reactions(
|
data class Reactions(
|
||||||
@ -15,4 +15,4 @@ data class Reactions(
|
|||||||
val rocket: Int = 0,
|
val rocket: Int = 0,
|
||||||
val total_count: Int = 0,
|
val total_count: Int = 0,
|
||||||
val url: String? = null
|
val url: String? = null
|
||||||
)
|
)
|
||||||
|
@ -22,4 +22,4 @@ data class Uploader(
|
|||||||
val subscriptions_url: String,
|
val subscriptions_url: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val url: String
|
val url: String
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package models.matomo
|
package models.matomo
|
||||||
|
|
||||||
typealias MatomoDownloads = ArrayList<MatomoDownloadsItem>
|
typealias MatomoDownloads = ArrayList<MatomoDownloadsItem>
|
||||||
|
@ -9,4 +9,4 @@ data class MatomoDownloadsItem(
|
|||||||
val nb_hits: Int = 0,
|
val nb_hits: Int = 0,
|
||||||
val nb_visits: Int = 0,
|
val nb_visits: Int = 0,
|
||||||
val sum_time_spent: Int = 0
|
val sum_time_spent: Int = 0
|
||||||
)
|
)
|
||||||
|
@ -54,13 +54,13 @@ internal suspend fun getAnalyticsImage(): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
|
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
|
// FAIL Gracefully
|
||||||
throw(RETRY_LIMIT_EXHAUSTED())
|
throw(RETRY_LIMIT_EXHAUSTED())
|
||||||
}
|
}
|
||||||
}while (contentLength<1_20_000)
|
} while (contentLength <1_20_000)
|
||||||
|
|
||||||
return analyticsImage
|
return analyticsImage
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,15 @@ internal suspend fun updateDownloadCards(
|
|||||||
fileName = "README.md"
|
fileName = "README.md"
|
||||||
).decryptedContent
|
).decryptedContent
|
||||||
|
|
||||||
var totalDownloads:Int = GithubService.getGithubRepoReleasesInfo(
|
var totalDownloads: Int = GithubService.getGithubRepoReleasesInfo(
|
||||||
secrets.ownerName,
|
secrets.ownerName,
|
||||||
secrets.repoName
|
secrets.repoName
|
||||||
).let { allReleases ->
|
).let { allReleases ->
|
||||||
var totalCount = 0
|
var totalCount = 0
|
||||||
|
|
||||||
for(release in allReleases){
|
for (release in allReleases) {
|
||||||
release.assets.forEach {
|
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
|
totalCount += it.download_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,19 +75,18 @@ private suspend fun getDownloadCard(
|
|||||||
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
|
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
|
||||||
// debug(contentLength.toString())
|
// debug(contentLength.toString())
|
||||||
|
|
||||||
if(retryCount-- == 0){
|
if (retryCount-- == 0) {
|
||||||
// FAIL Gracefully
|
// FAIL Gracefully
|
||||||
throw(RETRY_LIMIT_EXHAUSTED())
|
throw(RETRY_LIMIT_EXHAUSTED())
|
||||||
}
|
}
|
||||||
}while (contentLength<40_000)
|
} while (contentLength <40_000)
|
||||||
return downloadCard
|
return downloadCard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getDownloadCardHtml(
|
fun getDownloadCardHtml(
|
||||||
count: Int,
|
count: Int,
|
||||||
date: String, // ex: 06 Jun 2021
|
date: String, // ex: 06 Jun 2021
|
||||||
):String {
|
): String {
|
||||||
return """
|
return """
|
||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<div id="card" class="dark-bg">
|
<div id="card" class="dark-bg">
|
||||||
|
@ -6,4 +6,4 @@ val String.byProperty: String get() = System.getenv(this)
|
|||||||
val String.byOptionalProperty: String? get() = System.getenv(this)
|
val String.byOptionalProperty: String? get() = System.getenv(this)
|
||||||
|
|
||||||
fun debug(message: String) = println("\n::debug::$message")
|
fun debug(message: String) = println("\n::debug::$message")
|
||||||
fun debug(tag: String, message: String) = println("\n::debug::$tag:\n$message")
|
fun debug(tag: String, message: String) = println("\n::debug::$tag:\n$message")
|
||||||
|
Loading…
Reference in New Issue
Block a user