Maintenance Tasks, workflows, code cleanup

This commit is contained in:
shabinder 2021-05-18 20:02:33 +05:30
parent 68dc52c6a5
commit e7a983fe00
73 changed files with 305 additions and 299 deletions

35
.github/workflows/maintenance.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Maintenance
on:
workflow_dispatch:
schedule:
- cron: '*/30 * * * *' #every 30 minutes
jobs:
build:
name: Test and Build
runs-on: ubuntu-latest
steps:
# Setup Java environment for the next steps
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 15
# Check out current repository
- name: Fetch Sources
uses: actions/checkout@v2.3.1
# Run Maintenance Tasks
- name: Android App
run: ./gradlew :maintenance-tasks:run
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
OWNER_NAME: 'Shabinder'
REPO_NAME: 'SpotiFlyer'
BRANCH_NAME: 'main'
FILE_PATH: 'README.md'
IMAGE_DESCRIPTION: 'Analytics'
COMMIT_MESSAGE: 'Analytics Updated'
TAG_NAME: 'HTI'

3
.gitignore vendored
View File

@ -10,4 +10,5 @@ terraform.tfvars
/fastlane/report.xml
/fastlane/README.md
Gemfile
Gemfile.lock
Gemfile.lock
/maintenance-tasks/build/

View File

@ -77,7 +77,7 @@ android {
}
compileOptions {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
coreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@ -31,9 +31,8 @@ repositories {
}
dependencies {
implementation("com.android.tools.build:gradle:4.2.0")
implementation("com.android.tools.build:gradle:4.0.2")
implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}")
//implementation("io.github.gradle-nexus.publish-plugin:1.1.0")
implementation(JetBrains.Compose.gradlePlugin)
implementation(JetBrains.Kotlin.gradlePlugin)
implementation(JetBrains.Kotlin.serialization)

View File

@ -36,7 +36,7 @@ object Versions {
// Internet
const val ktor = "1.5.4"
const val kotlinxSerialization = "1.2.0"
const val kotlinxSerialization = "1.2.1"
// Database
const val sqlDelight = "1.5.0"
@ -158,7 +158,7 @@ object JetpackDataStore {
}
object Serialization {
val core = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.kotlinxSerialization}"
val json = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}"
}
object SqlDelight {

View File

@ -28,7 +28,6 @@ import androidx.compose.ui.text.font.FontWeight
import com.shabinder.common.database.R
import kotlinx.coroutines.flow.MutableStateFlow
actual fun montserratFont() = FontFamily(
Font(R.font.montserrat_light, FontWeight.Light),
Font(R.font.montserrat_regular, FontWeight.Normal),

View File

@ -3,7 +3,6 @@ package com.shabinder.common.uikit
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -30,4 +29,4 @@ actual fun VerticalScrollbar(
modifier: Modifier,
adapter: ScrollbarAdapter
) {
}
}

View File

@ -10,37 +10,24 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.AlertDialog
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CardGiftcard
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.shabinder.common.models.methods
import com.shabinder.common.uikit.PaypalLogo
import com.shabinder.common.uikit.RazorPay
import com.shabinder.common.uikit.SpotiFlyerShapes
import com.shabinder.common.uikit.SpotiFlyerTypography
import com.shabinder.common.uikit.colorAccent
@OptIn(ExperimentalAnimationApi::class)
@Composable
actual fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
){
isVisible: Boolean,
onDismiss: () -> Unit
) {
AnimatedVisibility(
isVisible
) {
@ -84,10 +71,12 @@ actual fun DonationDialog(
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
.clickable(onClick = {
onDismiss()
methods.value.giveDonation()
}),
.clickable(
onClick = {
onDismiss()
methods.value.giveDonation()
}
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(RazorPay(), "Indian Rupee Logo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
@ -138,4 +127,4 @@ actual fun DonationDialog(
onDismissRequest = onDismiss
)*/
}
}
}

View File

@ -2,7 +2,12 @@ package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
@ -10,7 +15,6 @@ import com.shabinder.common.di.Picture
import com.shabinder.common.di.dispatcherIO
import kotlinx.coroutines.withContext
@Composable
actual fun ImageLoad(
link: String,

View File

@ -20,7 +20,6 @@ package com.shabinder.common.uikit
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import com.shabinder.common.di.Picture
@Composable
expect fun DownloadImageTick()
@ -69,6 +68,6 @@ expect fun DownloadImageArrow(modifier: Modifier)
@Composable
expect fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
)
isVisible: Boolean,
onDismiss: () -> Unit
)

View File

@ -11,4 +11,4 @@ expect fun ImageLoad(
desc: String = "Album Art",
modifier: Modifier = Modifier,
// placeholder:Painter = PlaceHolderImage()
)
)

View File

@ -3,7 +3,6 @@ package com.shabinder.common.uikit
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@ -27,4 +26,4 @@ expect fun rememberScrollbarAdapter(
expect fun VerticalScrollbar(
modifier: Modifier,
adapter: ScrollbarAdapter
)
)

View File

@ -223,7 +223,7 @@ fun SearchPanel(
@Composable
fun AboutColumn(
modifier: Modifier = Modifier,
donationDialogOpenEvent:() -> Unit
donationDialogOpenEvent: () -> Unit
) {
Box {
@ -342,10 +342,12 @@ fun AboutColumn(
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = {
isDonationDialogVisible = true
donationDialogOpenEvent()
}),
.clickable(
onClick = {
isDonationDialogVisible = true
donationDialogOpenEvent()
}
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
@ -357,7 +359,7 @@ fun AboutColumn(
)
Text(
text = "If you think I deserve to get paid for my work, you can support me here.",
//text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.",
// text = "SpotiFlyer will always be, Free and Open-Source. You can however show us that you care by sending a small donation.",
style = SpotiFlyerTypography.subtitle2
)
}

View File

@ -37,15 +37,16 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -110,7 +111,7 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, modifier: Modifier = Modifi
}
@Composable
fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp, component: SpotiFlyerRoot) {
fun MainScreen(modifier: Modifier = Modifier, alpha: Float, topPadding: Dp = 0.dp, component: SpotiFlyerRoot) {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
@ -128,7 +129,7 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
val callBacks = component.callBacks
AppBar(
backgroundColor = appBarColor,
onBackPressed = callBacks::popBackToHomeScreen ,
onBackPressed = callBacks::popBackToHomeScreen,
setDownloadDirectory = callBacks::setDownloadDirectory,
isBackButtonVisible = activeComponent.value.activeChild.instance is Child.List,
modifier = Modifier.fillMaxWidth()
@ -150,8 +151,8 @@ fun MainScreen(modifier: Modifier = Modifier, alpha: Float,topPadding: Dp = 0.dp
@Composable
fun AppBar(
backgroundColor: Color,
onBackPressed:()->Unit,
setDownloadDirectory:()->Unit,
onBackPressed: () -> Unit,
setDownloadDirectory: () -> Unit,
isBackButtonVisible: Boolean,
modifier: Modifier = Modifier
) {
@ -181,11 +182,11 @@ fun AppBar(
}
},
actions = {
IconButton(
onClick = { setDownloadDirectory() }
) {
Icon(Icons.Filled.Settings,"Preferences", tint = Color.Gray)
}
IconButton(
onClick = { setDownloadDirectory() }
) {
Icon(Icons.Filled.Settings, "Preferences", tint = Color.Gray)
}
},
modifier = modifier,
elevation = 0.dp

View File

@ -17,8 +17,6 @@
package com.shabinder.common.uikit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.flow.MutableStateFlow
enum class ToastDuration(val value: Int) {

View File

@ -1,2 +1 @@
package com.shabinder.common.uikit.dialogs

View File

@ -17,27 +17,17 @@
@file:Suppress("FunctionName")
package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.vectorXmlResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.Font
import com.shabinder.common.di.Picture
import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.models.methods
import kotlinx.coroutines.withContext
@Composable
actual fun DownloadImageTick() {
@ -79,11 +69,11 @@ actual fun DownloadImageArrow(modifier: Modifier) {
actual fun DownloadAllImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_download_arrow.xml")) as Painter
@Composable
actual fun ShareImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_share_open.xml")) as Painter
actual fun ShareImage() = rememberVectorPainter(vectorXmlResource("drawable/ic_share_open.xml")) as Painter
@Composable
actual fun PlaceHolderImage() = rememberVectorPainter(vectorXmlResource("drawable/music.xml"))
as Painter
as Painter
@Composable
actual fun SpotiFlyerLogo() =
rememberVectorPainter(vectorXmlResource("drawable/ic_spotiflyer_logo.xml")) as Painter
@ -92,10 +82,10 @@ actual fun SpotiFlyerLogo() =
actual fun HeartIcon() = rememberVectorPainter(vectorXmlResource("drawable/ic_heart.xml")) as Painter
@Composable
actual fun SpotifyLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_spotify_logo.xml")) as Painter
actual fun SpotifyLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_spotify_logo.xml")) as Painter
@Composable
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
actual fun YoutubeLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_youtube.xml")) as Painter
@Composable
actual fun GaanaLogo() = rememberVectorPainter(vectorXmlResource("drawable/ic_gaana.xml")) as Painter

View File

@ -43,4 +43,4 @@ actual fun VerticalScrollbar(
modifier = modifier,
adapter = adapter
)
}
}

View File

@ -26,16 +26,13 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -57,7 +54,7 @@ actual fun Toast(
val state = flow.collectAsState("")
val message = state.value
AnimatedVisibility (
AnimatedVisibility(
visible = message != "",
enter = fadeIn() + slideInVertically(initialOffsetY = { it / 4 }),
exit = slideOutHorizontally(targetOffsetX = { it / 4 }) + fadeOut()
@ -67,12 +64,12 @@ actual fun Toast(
contentAlignment = Alignment.BottomEnd
) {
Surface(
modifier = Modifier.sizeIn(maxWidth = 250.dp,maxHeight = 80.dp),
modifier = Modifier.sizeIn(maxWidth = 250.dp, maxHeight = 80.dp),
color = Color(23, 23, 23),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, colorOffWhite)
) {
Box(contentAlignment = Alignment.Center,modifier = Modifier.fillMaxSize()){
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Text(
text = message,
color = Color(210, 210, 210),

View File

@ -26,9 +26,9 @@ import com.shabinder.common.models.methods
@OptIn(ExperimentalAnimationApi::class)
@Composable
actual fun DonationDialog(
isVisible:Boolean,
onDismiss:()->Unit
){
isVisible: Boolean,
onDismiss: () -> Unit
) {
AnimatedVisibility(
isVisible
) {
@ -72,10 +72,12 @@ actual fun DonationDialog(
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 6.dp)
.clickable(onClick = {
onDismiss()
methods.value.giveDonation()
}),
.clickable(
onClick = {
onDismiss()
methods.value.giveDonation()
}
),
verticalAlignment = Alignment.CenterVertically
) {
Icon(RazorPay(), "Indian Rupee Logo", Modifier.size(32.dp), tint = Color(0xFFCCCCCC))
@ -95,4 +97,4 @@ actual fun DonationDialog(
}
}
}
}
}

View File

@ -1,9 +1,13 @@
package com.shabinder.common.uikit
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T)
actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -17,7 +17,7 @@ actual interface PlatformActions {
fun sendTracksToService(array: ArrayList<TrackDetails>)
}
actual val StubPlatformActions = object: PlatformActions {
actual val StubPlatformActions = object : PlatformActions {
override val imageCacheDir = ""
override val sharedPreferences: SharedPreferences? = null
@ -25,4 +25,4 @@ actual val StubPlatformActions = object: PlatformActions {
override fun addToLibrary(path: String) {}
override fun sendTracksToService(array: ArrayList<TrackDetails>) {}
}
}

View File

@ -185,7 +185,7 @@ internal class RealCache<Key : Any, Value : Any>(
*/
private fun CacheEntry<Key, Value>.isExpired(): Boolean {
return expiresAfterAccess && (accessTimeMark.get() + expireAfterAccessDuration).hasPassedNow() ||
expiresAfterWrite && (writeTimeMark.get() + expireAfterWriteDuration).hasPassedNow()
expiresAfterWrite && (writeTimeMark.get() + expireAfterWriteDuration).hasPassedNow()
}
/**

View File

@ -41,8 +41,7 @@ interface Actions {
fun writeMp3Tags(trackDetails: TrackDetails)
}
private fun stubActions(): Actions = object :Actions {
private fun stubActions(): Actions = object : Actions {
override val platformActions = StubPlatformActions
override fun showPopUpMessage(string: String, long: Boolean) {}
override fun setDownloadDirectoryAction() {}
@ -53,4 +52,4 @@ private fun stubActions(): Actions = object :Actions {
override fun writeMp3Tags(trackDetails: TrackDetails) {}
override val isInternetAvailable: Boolean = true
}
}

View File

@ -20,4 +20,4 @@ sealed class AllPlatforms {
object Js : AllPlatforms()
object Jvm : AllPlatforms()
object Native : AllPlatforms()
}
}

View File

@ -2,4 +2,4 @@ package com.shabinder.common.models
expect interface PlatformActions
expect val StubPlatformActions : PlatformActions
expect val StubPlatformActions: PlatformActions

View File

@ -58,7 +58,5 @@ enum class Status constructor(val value: Int) {
else -> NONE
}
}
}
}
}

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T)
actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -1,5 +1,5 @@
package com.shabinder.common.models
actual interface PlatformActions {}
actual interface PlatformActions
actual val StubPlatformActions = object: PlatformActions {}
actual val StubPlatformActions = object : PlatformActions {}

View File

@ -2,8 +2,8 @@ package com.shabinder.common.models
import kotlin.native.concurrent.AtomicReference
actual interface PlatformActions {}
actual interface PlatformActions
actual val StubPlatformActions = object: PlatformActions {}
actual val StubPlatformActions = object : PlatformActions {}
actual typealias NativeAtomicReference<T> = AtomicReference<T>
actual typealias NativeAtomicReference<T> = AtomicReference<T>

View File

@ -1,4 +1,4 @@
package com.shabinder.common.models
actual interface PlatformActions {}
actual val StubPlatformActions = object: PlatformActions {}
actual interface PlatformActions
actual val StubPlatformActions = object : PlatformActions {}

View File

@ -1,3 +1,3 @@
package com.shabinder.common.models
actual class NativeAtomicReference<T> actual constructor(actual var value: T)
actual class NativeAtomicReference<T> actual constructor(actual var value: T)

View File

@ -54,7 +54,7 @@ kotlin {
}
}
if(HostOS.isMac) {
if (HostOS.isMac) {
val iosMain by getting {
dependencies {
implementation(SqlDelight.nativeDriver)

View File

@ -3,8 +3,8 @@ package com.shabinder.common.database
import co.touchlab.kermit.Logger
import co.touchlab.kermit.NSLogLogger
import com.shabinder.database.Database
import org.koin.dsl.module
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
import org.koin.dsl.module
@Suppress("RedundantNullableReturnType")
actual fun databaseModule() = module {
@ -14,4 +14,4 @@ actual fun databaseModule() = module {
}
}
actual fun getLogger(): Logger = NSLogLogger()
actual fun getLogger(): Logger = NSLogLogger()

View File

@ -18,7 +18,6 @@ package com.shabinder.common.database
import co.touchlab.kermit.CommonLogger
import co.touchlab.kermit.Logger
import com.shabinder.database.Database
import org.koin.dsl.module
actual fun databaseModule() = module { single { SpotiFlyerDatabase(null) } }

View File

@ -36,4 +36,4 @@ actual suspend fun downloadTracks(
if (!list.isNullOrEmpty()) {
methods.value.platformActions.sendTracksToService(ArrayList(list))
}
}
}

View File

@ -54,10 +54,10 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch"
}
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone(){
settings.putBoolean(firstLaunch,false)
actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch, false)
}
/*
@ -67,10 +67,10 @@ actual class Dir actual constructor(
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true)
settings.putBoolean(AnalyticsKey, true)
}
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
@Suppress("DEPRECATION")
private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
@ -81,7 +81,7 @@ actual class Dir actual constructor(
// fun call in order to always access Updated Value
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
File.separator + "SpotiFlyer" + File.separator
File.separator + "SpotiFlyer" + File.separator
actual fun isPresent(path: String): Boolean = File(path).exists()
@ -106,14 +106,14 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit
) = withContext(dispatcherIO) {
postProcess: (track: TrackDetails) -> Unit
) = withContext(dispatcherIO) {
val songFile = File(trackDetails.outputFilePath)
try {
/*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */
if(!songFile.exists()) {
if (!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile?.mkdirs()
}
@ -163,15 +163,15 @@ actual class Dir actual constructor(
} catch (e: Exception) { e.printStackTrace() }
}
}
}catch (e:Exception){
if(songFile.exists()) songFile.delete()
} catch (e: Exception) {
if (songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" }
}
}
actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path)
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO) {
val cachePath = imageCacheDir() + getNameURL(url)
Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap())
}
@ -189,7 +189,7 @@ actual class Dir actual constructor(
}
@Suppress("BlockingMethodInNonBlockingContext")
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) {
actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
try {
FileOutputStream(path).use { out ->
(image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out)

View File

@ -24,7 +24,10 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkRequest
import android.util.Log
import androidx.lifecycle.LiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import java.lang.Exception
import java.net.InetSocketAddress
@ -104,7 +107,7 @@ class ConnectionLiveData(context: Context) : LiveData<Boolean>() {
* If successful, that means we have internet.
*/
object DoesNetworkHaveInternet {
suspend fun execute(network: Network): Boolean = withContext(Dispatchers.IO) {
suspend fun execute(network: Network): Boolean = withContext(Dispatchers.IO) {
try {
Log.d(TAG, "PINGING google.")
val socket = network.socketFactory.createSocket() ?: throw IOException("Socket is null.")
@ -112,7 +115,7 @@ class ConnectionLiveData(context: Context) : LiveData<Boolean>() {
socket.close()
Log.d(TAG, "PING success.")
true
}catch (e:Exception){
} catch (e: Exception) {
e.printStackTrace()
// Handle VPN Connection / Google DNS Blocked Cases
isInternetAccessible()

View File

@ -17,13 +17,13 @@
package com.shabinder.common.di
import android.util.Log
import com.shabinder.common.models.TrackDetails
import java.io.File
import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.collect
import java.io.File
import java.io.FileInputStream
fun Mp3File.removeAllTags(): Mp3File {

View File

@ -33,7 +33,10 @@ import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.*
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.R
import com.shabinder.common.di.downloadFile
import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult
@ -120,7 +123,8 @@ class ForegroundService : Service(), CoroutineScope {
val downloadObjects: ArrayList<TrackDetails>? = (
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object")
"object"
)
)
downloadObjects?.let { list ->
@ -184,7 +188,7 @@ class ForegroundService : Service(), CoroutineScope {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if (url == null) {
val audioData: Format = ytDownloader?.getVideo(videoID)?.get() ?: throw Exception("Java YT Dependency Error")
val ytUrl = audioData.url!! //We Will catch NPE
val ytUrl = audioData.url!! // We Will catch NPE
enqueueDownload(ytUrl, track)
} else enqueueDownload(url, track)
} catch (e: Exception) {
@ -210,7 +214,7 @@ class ForegroundService : Service(), CoroutineScope {
removeFromNotification("Downloading ${track.title}")
failed++
updateNotification()
sendTrackBroadcast(Status.FAILED.name,track)
sendTrackBroadcast(Status.FAILED.name, track)
}
}
@ -229,7 +233,7 @@ class ForegroundService : Service(), CoroutineScope {
is DownloadResult.Success -> {
try {
// Save File and Embed Metadata
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track){} }
val job = launch(Dispatchers.Default) { dir.saveFileWithMetadata(it.byteArray, track) {} }
allTracksStatus[track.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting", track)
addToNotification("Processing ${track.title}")
@ -288,7 +292,7 @@ class ForegroundService : Service(), CoroutineScope {
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
downloadService.close()
updateNotification()
cleanFiles(File(dir.defaultDir()),logger)
cleanFiles(File(dir.defaultDir()), logger)
// TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("", "", "", "", "")
releaseWakeLock()

View File

@ -8,7 +8,7 @@ import java.io.File
/**
* Cleaning All Residual Files except Mp3 Files
**/
fun cleanFiles(dir: File,logger: Kermit) {
fun cleanFiles(dir: File, logger: Kermit) {
try {
logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " }
val fList = dir.listFiles()
@ -24,12 +24,12 @@ fun cleanFiles(dir: File,logger: Kermit) {
}
}
}
} catch (e:Exception) { e.printStackTrace() }
} catch (e: Exception) { e.printStackTrace() }
}
/**
* Cleaning All Residual Files except Mp3 Files
**/
fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) {
fun cleanFiles(directory: AbstractFile, fm: FileManager, logger: Kermit) {
try {
logger.d("Files Cleaning") { "Starting Cleaning in ${directory.getFullPath()} " }
val fList = fm.listFiles(directory)
@ -37,14 +37,13 @@ fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) {
if (fm.isDirectory(file)) {
cleanFiles(file, fm, logger)
} else if (fm.isFile(file)) {
if (file.getFullPath().substringAfterLast(".") != "mp3"
||
if (file.getFullPath().substringAfterLast(".") != "mp3" ||
fm.getLength(file) == 0L
) {
) {
logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" }
fm.delete(file)
}
}
}
} catch (e:Exception) { e.printStackTrace() }
}
} catch (e: Exception) { e.printStackTrace() }
}

View File

@ -25,10 +25,13 @@ import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.HttpClient
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.KotlinxSerializer
import io.ktor.client.features.logging.DEFAULT
import io.ktor.client.features.logging.LogLevel
import io.ktor.client.features.logging.Logger
import io.ktor.client.features.logging.Logging
import kotlinx.serialization.json.Json
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration

View File

@ -23,7 +23,8 @@ import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.database.Database
import io.ktor.client.request.*
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.statement.HttpStatement
import io.ktor.http.contentLength
import io.ktor.http.isSuccess
@ -31,14 +32,14 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt
expect class Dir (
expect class Dir(
logger: Kermit,
settings: Settings,
spotiFlyerDatabase: SpotiFlyerDatabase,
) {
val db: Database?
val isAnalyticsEnabled:Boolean
val isFirstLaunch:Boolean
val isAnalyticsEnabled: Boolean
val isFirstLaunch: Boolean
fun enableAnalytics()
fun firstLaunchDone()
fun isPresent(path: String): Boolean
@ -46,11 +47,11 @@ expect class Dir (
fun defaultDir(): String
fun imageCacheDir(): String
fun createDirectory(dirPath: String)
fun setDownloadDirectory(newBasePath:String)
fun setDownloadDirectory(newBasePath: String)
suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage
suspend fun loadImage(url: String): Picture
suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(track: TrackDetails)->Unit = {})
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit = {})
fun addToLibrary(path: String)
}
@ -74,7 +75,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
emit(DownloadResult.Error("File not downloaded"))
}
client.close()
} catch (e:Exception) {
} catch (e: Exception) {
e.printStackTrace()
emit(DownloadResult.Error(e.message ?: "File not downloaded"))
}
@ -83,12 +84,12 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
suspend fun downloadByteArray(
url: String,
httpBuilder: HttpRequestBuilder.()->Unit = {}
httpBuilder: HttpRequestBuilder.() -> Unit = {}
): ByteArray? {
val client = createHttpClient()
val response = try {
client.get<ByteArray>(url,httpBuilder)
} catch (e: Exception){
client.get<ByteArray>(url, httpBuilder)
} catch (e: Exception) {
return null
}
client.close()

View File

@ -18,8 +18,7 @@ package com.shabinder.common.di
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import io.ktor.client.request.*
import io.ktor.client.request.head
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -31,7 +30,6 @@ expect suspend fun downloadTracks(
dir: Dir
)
// IO-Dispatcher
@SharedImmutable
expect val dispatcherIO: CoroutineDispatcher

View File

@ -79,7 +79,7 @@ class GaanaProvider(
it.updateStatusIfPresent(folderType, subFolder)
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.track_title
coverUrl = it.artworkLink.replace("http:","https:")
coverUrl = it.artworkLink.replace("http:", "https:")
}
}
"album" -> {
@ -91,7 +91,7 @@ class GaanaProvider(
}
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
title = link
coverUrl = it.custom_artworks.size_480p.replace("http:","https:")
coverUrl = it.custom_artworks.size_480p.replace("http:", "https:")
}
}
"playlist" -> {
@ -114,7 +114,7 @@ class GaanaProvider(
getGaanaArtistDetails(seokey = link).artist.firstOrNull()
?.also {
title = it.name
coverUrl = it.artworkLink?.replace("http:","https:") ?: gaanaPlaceholderImageUrl
coverUrl = it.artworkLink?.replace("http:", "https:") ?: gaanaPlaceholderImageUrl
}
getGaanaArtistTracks(seokey = link).also {
it.tracks?.forEach { track ->
@ -143,7 +143,7 @@ class GaanaProvider(
trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana,
albumArtURL = it.artworkLink.replace("http:","https:"),
albumArtURL = it.artworkLink.replace("http:", "https:"),
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
)
}

View File

@ -62,9 +62,9 @@ class SpotifyProvider(
defaultRequest {
header("Authorization", "Bearer ${token.access_token}")
}
install(JsonFeature) {
install(JsonFeature) {
serializer = KotlinxSerializer(globalJson)
}
}
}.also { httpClientRef.value = it }
}
}
@ -98,7 +98,7 @@ class SpotifyProvider(
type,
link
)
}catch (e: Exception){
} catch (e: Exception) {
e.printStackTrace()
// Try Reinitialising Client // Handle 401 Token Expiry ,etc Exceptions
authenticateSpotifyClient(true)
@ -108,7 +108,7 @@ class SpotifyProvider(
type,
link
)
} catch (e:Exception){
} catch (e: Exception) {
e.printStackTrace()
null
}

View File

@ -21,7 +21,6 @@ import com.shabinder.common.di.Dir
import com.shabinder.common.di.currentPlatform
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.methods
import io.ktor.client.HttpClient
class YoutubeMp3(

View File

@ -54,7 +54,7 @@ class YoutubeMusic constructor(
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
} catch (e:Exception) {
} catch (e: Exception) {
// All Internet/Client Related Errors
e.printStackTrace()
null

View File

@ -38,7 +38,7 @@ class YoutubeProvider(
private val dir: Dir,
) {
// Youtube Downloader isn't fully compatible with JS Yet
val ytDownloader: YoutubeDownloader? = if(currentPlatform == AllPlatforms.Js) null else YoutubeDownloader()
val ytDownloader: YoutubeDownloader? = if (currentPlatform == AllPlatforms.Js) null else YoutubeDownloader()
/*
* YT Album Art Schema
@ -102,25 +102,25 @@ class YoutubeProvider(
val videos = playlist.videos
coverUrl = "https://i.ytimg.com/vi/${
videos.firstOrNull()?.videoId
videos.firstOrNull()?.videoId
}/hqdefault.jpg"
title = name
trackList = videos.map {
TrackDetails(
title = it.title ?: "N/A",
artists = listOf(it.author ?: "N/A"),
artists = listOf(it.author ?: "N/A"),
durationSec = it.lengthSeconds,
albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
downloaded = if (dir.isPresent(
dir.finalOutputDir(
itemName = it.title ?: "N/A",
type = folderType,
subFolder = subFolder,
dir.defaultDir()
)
itemName = it.title ?: "N/A",
type = folderType,
subFolder = subFolder,
dir.defaultDir()
)
)
)
DownloadStatus.Downloaded
@ -170,11 +170,11 @@ class YoutubeProvider(
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (dir.isPresent(
dir.finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder,
defaultDir = dir.defaultDir()
)
itemName = name,
type = folderType,
subFolder = subFolder,
defaultDir = dir.defaultDir()
)
)
)
DownloadStatus.Downloaded

View File

@ -34,7 +34,7 @@ suspend fun authenticateSpotify(): TokenData? {
if (methods.value.isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
} else null
}catch (e:Exception) {
} catch (e: Exception) {
e.printStackTrace()
null
}

View File

@ -23,14 +23,14 @@ import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
import com.shabinder.common.models.spotify.Playlist
import com.shabinder.common.models.spotify.Track
import io.ktor.client.HttpClient
import io.ktor.client.request.*
import io.ktor.client.request.get
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
interface SpotifyRequests {
val httpClientRef: NativeAtomicReference<HttpClient>
val httpClient:HttpClient get() = httpClientRef.value
val httpClient: HttpClient get() = httpClientRef.value
suspend fun authenticateSpotifyClient(override: Boolean = false)

View File

@ -22,7 +22,6 @@ package com.shabinder.common.di.utils
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.models.methods
import io.ktor.utils.io.core.Closeable
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CancellationException

View File

@ -16,21 +16,16 @@
package com.shabinder.common.di.utils
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import kotlin.native.concurrent.SharedImmutable
import kotlin.native.concurrent.ThreadLocal
@ThreadLocal
val json by lazy { Json {
isLenient = true
ignoreUnknownKeys = true
} }
val json by lazy {
Json {
isLenient = true
ignoreUnknownKeys = true
}
}
/**
* Removing Illegal Chars from File Name

View File

@ -59,7 +59,7 @@ actual suspend fun downloadTracks(
) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) }
)
} else { // Found Youtube Video ID
downloadTrack(videoId, it, dir::saveFileWithMetadata,fetcher.youtubeMp3)
downloadTrack(videoId, it, dir::saveFileWithMetadata, fetcher.youtubeMp3)
}
}
}
@ -102,7 +102,7 @@ suspend fun downloadTrack(
)
}
is DownloadResult.Success -> { // Todo clear map
saveFileWithMetaData(it.byteArray, trackDetails){}
saveFileWithMetaData(it.byteArray, trackDetails) {}
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0

View File

@ -50,16 +50,16 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch"
}
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone(){
settings.putBoolean(firstLaunch,false)
actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch, false)
}
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true)
settings.putBoolean(AnalyticsKey, true)
}
init {
@ -76,7 +76,7 @@ actual class Dir actual constructor(
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + fileSeparator() +
"SpotiFlyer" + fileSeparator()
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
actual fun isPresent(path: String): Boolean = File(path).exists()
@ -110,19 +110,19 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit
postProcess: (track: TrackDetails) -> Unit
) {
val songFile = File(trackDetails.outputFilePath)
try {
/*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */
if(!songFile.exists()) {
if (!songFile.exists()) {
/*Make intermediate Dirs if they don't exist yet*/
songFile.parentFile.mkdirs()
}
if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
if (mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> {
@ -167,11 +167,11 @@ actual class Dir actual constructor(
} catch (e: Exception) { e.printStackTrace() }
}
}
}catch (e:Exception){
withContext(Dispatchers.Main){
//Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
withContext(Dispatchers.Main) {
// Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
}
if(songFile.exists()) songFile.delete()
if (songFile.exists()) songFile.delete()
logger.e { "${songFile.absolutePath} could not be created" }
}
}

View File

@ -1,6 +1,6 @@
package com.shabinder.common.di
import com.shabinder.common.di.providers.getData
import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
@ -30,7 +30,7 @@ actual suspend fun downloadTracks(
Downloader.execute {
if (!track.videoID.isNullOrBlank()) { // Video ID already known!
dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" }
downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata,fetcher)
downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata, fetcher)
} else {
val searchQuery = "${track.title} - ${track.artists.joinToString(",")}"
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, track)
@ -42,7 +42,7 @@ actual suspend fun downloadTracks(
) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
)
} else { // Found Youtube Video ID
downloadTrack(videoId, track, dir::saveFileWithMetadata,fetcher)
downloadTrack(videoId, track, dir::saveFileWithMetadata, fetcher)
}
}
}
@ -63,7 +63,7 @@ suspend fun downloadTrack(
fetcher.dir.logger.i { "LINK: $videoID -> $link" }
if (link == null) {
link = fetcher.youtubeProvider.ytDownloader.getVideo(videoID).get()?.url ?: return
link = fetcher.youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url ?: return
}
fetcher.dir.logger.i { "LINK: $videoID -> $link" }
downloadFile(link).collect {
@ -81,7 +81,7 @@ suspend fun downloadTrack(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.toMutableMap().apply {
set(trackDetails.title,DownloadStatus.Downloading(it.progress))
set(trackDetails.title, DownloadStatus.Downloading(it.progress))
}
}
is DownloadResult.Success -> { // Todo clear map

View File

@ -1,17 +1,15 @@
package com.shabinder.common.di
import com.shabinder.common.models.DownloadStatus
import kotlinx.coroutines.flow.MutableSharedFlow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
/*
* Dependency Provider for IOS
* */
object IOSDeps: KoinComponent {
object IOSDeps : KoinComponent {
val dir: Dir by inject() // = get()
val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get()
val database get() = dir.db
val sharedFlow = DownloadProgressFlow
val defaultDispatcher = dispatcherDefault
}
}

View File

@ -33,71 +33,71 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch"
}
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false)
settings.putBoolean(firstLaunch, false)
}
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true)
settings.putBoolean(AnalyticsKey, true)
}
actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path)
actual fun fileSeparator(): String = "/"
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!!.path!!
private val defaultBaseDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!.path!!
// TODO Error Handling
actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) +
fileSeparator() + "SpotiFlyer" + fileSeparator()
fileSeparator() + "SpotiFlyer" + fileSeparator()
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
private val defaultDirURL: NSURL by lazy {
val musicDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask,null,true,null)!!
musicDir.URLByAppendingPathComponent("SpotiFlyer",true)!!
val musicDir = NSFileManager.defaultManager.URLForDirectory(NSMusicDirectory, NSUserDomainMask, null, true, null)!!
musicDir.URLByAppendingPathComponent("SpotiFlyer", true)!!
}
actual fun imageCacheDir(): String = imageCacheURL.path!! + fileSeparator()
private val imageCacheURL: NSURL by lazy {
val cacheDir = NSFileManager.defaultManager.URLForDirectory(NSCachesDirectory, NSUserDomainMask,null,true,null)
cacheDir?.URLByAppendingPathComponent("SpotiFlyer",true)!!
val cacheDir = NSFileManager.defaultManager.URLForDirectory(NSCachesDirectory, NSUserDomainMask, null, true, null)
cacheDir?.URLByAppendingPathComponent("SpotiFlyer", true)!!
}
actual fun createDirectory(dirPath: String) {
try {
NSFileManager.defaultManager.createDirectoryAtPath(dirPath,true,null,null)
} catch (e:Exception) {
NSFileManager.defaultManager.createDirectoryAtPath(dirPath, true, null, null)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun createDirectory(dirURL: NSURL) {
try {
NSFileManager.defaultManager.createDirectoryAtURL(dirURL,true,null,null)
} catch (e:Exception) {
NSFileManager.defaultManager.createDirectoryAtURL(dirURL, true, null, null)
} catch (e: Exception) {
e.printStackTrace()
}
}
actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO){
actual suspend fun cacheImage(image: Any, path: String): Unit = withContext(dispatcherIO) {
try {
(image as? UIImage)?.let {
// We Will Be Using JPEG as default format everywhere
UIImageJPEGRepresentation(it,1.0)
?.writeToFile(path,true)
UIImageJPEGRepresentation(it, 1.0)
?.writeToFile(path, true)
}
}catch (e:Exception){
} catch (e: Exception) {
e.printStackTrace()
}
}
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO) {
try {
val cachePath = imageCacheURL.URLByAppendingPathComponent(getNameURL(url))
Picture(image = cachePath?.path?.let { loadCachedImage(it) } ?: loadFreshImage(url))
@ -110,24 +110,24 @@ actual class Dir actual constructor(
private fun loadCachedImage(filePath: String): UIImage? {
return try {
UIImage.imageWithContentsOfFile(filePath)
}catch (e:Exception) {
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private suspend fun loadFreshImage(url: String):UIImage? = withContext(dispatcherIO){
private suspend fun loadFreshImage(url: String): UIImage? = withContext(dispatcherIO) {
try {
val nsURL = NSURL(string = url)
val data = NSURLConnection.sendSynchronousRequest(NSURLRequest.requestWithURL(nsURL),null,null)
val data = NSURLConnection.sendSynchronousRequest(NSURLRequest.requestWithURL(nsURL), null, null)
if (data != null) {
UIImage.imageWithData(data)?.also {
GlobalScope.launch {
cacheImage(it, imageCacheDir() + getNameURL(url))
}
}
}else null
}catch (e: Exception) {
} else null
} catch (e: Exception) {
e.printStackTrace()
null
}
@ -136,7 +136,8 @@ actual class Dir actual constructor(
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
try {
val fileManager = NSFileManager.defaultManager
val paths = fileManager.contentsOfDirectoryAtURL(imageCacheURL,
val paths = fileManager.contentsOfDirectoryAtURL(
imageCacheURL,
null,
NSDirectoryEnumerationSkipsHiddenFiles,
null
@ -144,20 +145,19 @@ actual class Dir actual constructor(
paths?.forEach {
(it as? NSURL)?.let { nsURL ->
// Lets Remove Cached File
fileManager.removeItemAtURL(nsURL,null)
fileManager.removeItemAtURL(nsURL, null)
}
}
}catch (e: Exception) {
} catch (e: Exception) {
e.printStackTrace()
}
}
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit
) : Unit = withContext(dispatcherIO) {
postProcess: (track: TrackDetails) -> Unit
): Unit = withContext(dispatcherIO) {
try {
if (mp3ByteArray.isNotEmpty()) {
mp3ByteArray.toNSData().writeToFile(
@ -167,9 +167,9 @@ actual class Dir actual constructor(
}
when (trackDetails.outputFilePath.substringAfterLast('.')) {
".mp3" -> {
if(!isPresent(trackDetails.albumArtPath)) {
if (!isPresent(trackDetails.albumArtPath)) {
val imageData = downloadByteArray(
trackDetails.albumArtURL
trackDetails.albumArtURL
)?.toNSData()
if (imageData != null) {
UIImage.imageWithData(imageData)?.also {
@ -186,7 +186,7 @@ actual class Dir actual constructor(
)*/
}
}
}catch (e:Exception){
} catch (e: Exception) {
e.printStackTrace()
}
}
@ -196,4 +196,4 @@ actual class Dir actual constructor(
}
actual val db: Database? = spotiFlyerDatabase.instance
}
}

View File

@ -4,4 +4,4 @@ import platform.UIKit.UIImage
actual data class Picture(
val image: UIImage?
)
)

View File

@ -21,4 +21,4 @@ fun NSData.toByteArray(): ByteArray = memScoped {
val nsData = ByteArray(size)
memcpy(nsData.refTo(0), bytes, size.toULong())
return nsData
}
}

View File

@ -20,7 +20,6 @@ import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@ -75,7 +74,7 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPl
when (it) {
is DownloadResult.Success -> {
println("Download Completed")
dir.saveFileWithMetadata(it.byteArray, track){}
dir.saveFileWithMetadata(it.byteArray, track) {}
}
is DownloadResult.Error -> {
allTracksStatus[track.title] = DownloadStatus.Failed

View File

@ -43,19 +43,19 @@ actual class Dir actual constructor(
const val firstLaunch = "firstLaunch"
}
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual val isFirstLaunch get() = settings.getBooleanOrNull(firstLaunch) ?: true
actual fun firstLaunchDone() {
settings.putBoolean(firstLaunch,false)
settings.putBoolean(firstLaunch, false)
}
actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false
actual fun enableAnalytics() {
settings.putBoolean(AnalyticsKey,true)
settings.putBoolean(AnalyticsKey, true)
}
actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath)
actual fun setDownloadDirectory(newBasePath: String) = settings.putString(DirKey, newBasePath)
/*init {
createDirectories()
@ -84,7 +84,7 @@ actual class Dir actual constructor(
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit
postProcess: (track: TrackDetails) -> Unit
) {
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
val albumArt = downloadFile(corsApi + trackDetails.albumArtURL)

View File

@ -68,9 +68,7 @@ interface SpotiFlyerList {
val listAnalytics: Analytics
}
interface Analytics {
}
interface Analytics
sealed class Output {
object Finished : Output()

View File

@ -19,6 +19,7 @@ package com.shabinder.common.list.integration
import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue
import com.shabinder.common.list.SpotiFlyerList
@ -28,7 +29,6 @@ import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.list.store.SpotiFlyerListStoreProvider
import com.shabinder.common.list.store.getStore
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.caching.Cache
internal class SpotiFlyerListImpl(
componentContext: ComponentContext,

View File

@ -24,7 +24,6 @@ import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.dispatcherIO
import com.shabinder.common.di.downloadTracks
import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
@ -34,7 +33,6 @@ import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.withContext
internal class SpotiFlyerListStoreProvider(
private val dir: Dir,
@ -80,14 +78,14 @@ internal class SpotiFlyerListStoreProvider(
is Intent.SearchLink -> {
try {
val result = fetchQuery.query(link)
if( result != null) {
if (result != null) {
result.trackList = result.trackList.toMutableList()
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
executeIntent(Intent.RefreshTracksStatuses, getState)
} else {
throw Exception("An Error Occurred, Check your Link / Connection")
}
} catch (e:Exception) {
} catch (e: Exception) {
e.printStackTrace()
dispatch(Result.ErrorOccurred(e))
}
@ -101,7 +99,6 @@ internal class SpotiFlyerListStoreProvider(
}
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isNullOrEmpty()) methods.value.showPopUpMessage("All Songs are Processed")

View File

@ -19,6 +19,7 @@ package com.shabinder.common.main.integration
import co.touchlab.stately.ensureNeverFrozen
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.value.Value
import com.shabinder.common.caching.Cache
import com.shabinder.common.di.Picture
import com.shabinder.common.di.utils.asValue
import com.shabinder.common.main.SpotiFlyerMain
@ -30,7 +31,6 @@ import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
import com.shabinder.common.main.store.getStore
import com.shabinder.common.models.methods
import com.shabinder.common.caching.Cache
internal class SpotiFlyerMainImpl(
componentContext: ComponentContext,

View File

@ -38,17 +38,17 @@ fun org.jetbrains.kotlin.gradle.dsl.KotlinNativeBinaryContainer.generateFramewor
kotlin {
/*IOS Target Can be only built on Mac*/
if(HostOS.isMac) {
if (HostOS.isMac) {
val sdkName: String? = System.getenv("SDK_NAME")
val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
if (isiOSDevice) {
iosArm64("ios"){
iosArm64("ios") {
binaries {
generateFramework()
}
}
} else {
iosX64("ios"){
iosX64("ios") {
binaries {
generateFramework()
}
@ -68,7 +68,7 @@ kotlin {
}
}
}
if(HostOS.isMac){
if (HostOS.isMac) {
/*Required to Export `packForXcode`*/
sourceSets {
named("iosMain") {
@ -88,7 +88,7 @@ kotlin {
}
val packForXcode by tasks.creating(Sync::class) {
if(HostOS.isMac){
if (HostOS.isMac) {
group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val targetName = "ios"

View File

@ -30,7 +30,6 @@ import com.shabinder.common.root.SpotiFlyerRoot.Dependencies
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.database.Database
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -53,7 +52,7 @@ interface SpotiFlyerRoot {
val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir
val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>>
val actions:Actions
val actions: Actions
val analytics: Analytics
}

View File

@ -18,7 +18,7 @@ package com.shabinder.common.root.callbacks
interface SpotiFlyerRootCallBacks {
fun searchLink(link: String)
fun showToast(text:String)
fun showToast(text: String)
fun popBackToHomeScreen()
fun setDownloadDirectory()
}

View File

@ -48,8 +48,8 @@ import kotlinx.coroutines.launch
internal class SpotiFlyerRootImpl(
componentContext: ComponentContext,
private val main: (ComponentContext, output:Consumer<SpotiFlyerMain.Output>)->SpotiFlyerMain,
private val list: (ComponentContext, link:String, output:Consumer<SpotiFlyerList.Output>)->SpotiFlyerList,
private val main: (ComponentContext, output: Consumer<SpotiFlyerMain.Output>) -> SpotiFlyerMain,
private val list: (ComponentContext, link: String, output: Consumer<SpotiFlyerList.Output>) -> SpotiFlyerList,
private val actions: Actions,
private val analytics: Analytics
) : SpotiFlyerRoot, ComponentContext by componentContext {
@ -57,9 +57,9 @@ internal class SpotiFlyerRootImpl(
constructor(
componentContext: ComponentContext,
dependencies: Dependencies,
):this(
) : this(
componentContext = componentContext,
main = { childContext,output ->
main = { childContext, output ->
spotiFlyerMain(
childContext,
output,
@ -104,7 +104,7 @@ internal class SpotiFlyerRootImpl(
it !is Configuration.Main
}
}
override fun showToast(text:String) { toastState.value = text }
override fun showToast(text: String) { toastState.value = text }
override fun setDownloadDirectory() { actions.setDownloadDirectoryAction() }
}
@ -130,7 +130,7 @@ internal class SpotiFlyerRootImpl(
}
}
private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){
private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override: Boolean) {
GlobalScope.launch(Dispatchers.Default) {
analytics.appLaunchEvent()
/*Authenticate Spotify Client*/
@ -147,7 +147,7 @@ internal class SpotiFlyerRootImpl(
}
}
private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<SpotiFlyerMain.Output> ,dependencies: Dependencies): SpotiFlyerMain =
private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer<SpotiFlyerMain.Output>, dependencies: Dependencies): SpotiFlyerMain =
SpotiFlyerMain(
componentContext = componentContext,
dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies {

View File

@ -28,7 +28,7 @@ version = Versions.versionName
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "14"
kotlinOptions.jvmTarget = "1.8"
}
}

View File

@ -28,5 +28,6 @@ include(
":fuzzywuzzy:app",
":android",
":desktop",
":web-app"
)
":web-app",
":maintenance-tasks"
)