Jetpack DataStore, Dependency Injection.

This commit is contained in:
shabinder 2021-01-02 00:36:39 +05:30
parent df5f7dd2c5
commit 5c0ec20e1b
20 changed files with 367 additions and 416 deletions

View File

@ -134,6 +134,9 @@ dependencies {
implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version" implementation "dev.chrisbanes.accompanist:accompanist-coil:$coil_version"
implementation "dev.chrisbanes.accompanist:accompanist-insets:$coil_version" implementation "dev.chrisbanes.accompanist:accompanist-insets:$coil_version"
//DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha05"
//Extras //Extras
implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.mpatric:mp3agic:0.9.1'

View File

@ -41,10 +41,13 @@
android:launchMode="singleTask"> android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<data android:mimeType="text/plain" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity> </activity>
<!-- Add your API key here --> <!-- Add your API key here -->

View File

@ -28,8 +28,9 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.jetcaster.util.verticalGradientScrim import com.example.jetcaster.util.verticalGradientScrim
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.navigation.ComposeNavigation import com.shabinder.spotiflyer.navigation.ComposeNavigation
import com.shabinder.spotiflyer.navigation.navigateToPlatform import com.shabinder.spotiflyer.navigation.navigateToTrackList
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.ui.ComposeLearnTheme import com.shabinder.spotiflyer.ui.ComposeLearnTheme
@ -40,7 +41,10 @@ import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.chrisbanes.accompanist.insets.ProvideWindowInsets import dev.chrisbanes.accompanist.insets.ProvideWindowInsets
import dev.chrisbanes.accompanist.insets.statusBarsHeight import dev.chrisbanes.accompanist.insets.statusBarsHeight
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -69,14 +73,12 @@ class MainActivity : AppCompatActivity() {
ComposeLearnTheme { ComposeLearnTheme {
Providers(AmbientContentColor provides colorOffWhite) { Providers(AmbientContentColor provides colorOffWhite) {
ProvideWindowInsets { ProvideWindowInsets {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.6f) val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.7f)
navController = rememberNavController() navController = rememberNavController()
val gradientColor by sharedViewModel.gradientColor.collectAsState()
Column( Column(
modifier = Modifier.fillMaxSize().verticalGradientScrim( modifier = Modifier.fillMaxSize().verticalGradientScrim(
color = gradientColor.copy(alpha = 0.38f), color = sharedViewModel.gradientColor.copy(alpha = 0.38f),
startYPercentage = 1f, startYPercentage = 1f,
endYPercentage = 0f, endYPercentage = 0f,
fixedHeight = 700f, fixedHeight = 700f,
@ -101,7 +103,6 @@ class MainActivity : AppCompatActivity() {
} }
private fun initialize() { private fun initialize() {
authenticateSpotify()
requestStoragePermission() requestStoragePermission()
disableDozeMode() disableDozeMode()
//checkIfLatestVersion() //checkIfLatestVersion()
@ -147,50 +148,12 @@ class MainActivity : AppCompatActivity() {
} }
} }
/**
* Adding my own Spotify Web Api Requests!
* */
private fun implementSpotifyService(token: String) {
val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient.addInterceptor(Interceptor { chain ->
val request: Request =
chain.request().newBuilder().addHeader(
"Authorization",
"Bearer $token"
).build()
chain.proceed(request)
}).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder().run{
baseUrl("https://api.spotify.com/v1/")
client(httpClient.build())
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}
sharedViewModel.spotifyService.value = retrofit.create(SpotifyService::class.java)
}
fun authenticateSpotify() {
if(sharedViewModel.spotifyService.value == null){
sharedViewModel.viewModelScope.launch {
log("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken()
token.value?.let {
showDialog1("Success: Spotify Token Acquired")
implementSpotifyService(it.access_token)
}
log("Spotify Token", token.value.toString())
}
}
}
private fun handleIntentFromExternalActivity(intent: Intent? = getIntent()) { private fun handleIntentFromExternalActivity(intent: Intent? = getIntent()) {
if (intent?.action == Intent.ACTION_SEND) { if (intent?.action == Intent.ACTION_SEND) {
if ("text/plain" == intent.type) { if ("text/plain" == intent.type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
log("Intent Received", it) log("Intent Received", it)
navController.navigateToPlatform(it) navController.navigateToTrackList(it)
} }
} }
} }

View File

@ -17,41 +17,42 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import androidx.compose.material.MaterialTheme import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.networking.GaanaInterface import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.ui.colorPrimary
import com.shabinder.spotiflyer.ui.colorPrimaryDark import com.shabinder.spotiflyer.ui.colorPrimaryDark
import com.shabinder.spotiflyer.ui.home.HomeCategory
import dagger.hilt.android.scopes.ActivityRetainedScoped import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ActivityRetainedScoped @ActivityRetainedScoped
class SharedViewModel @ViewModelInject constructor( class SharedViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO, val databaseDAO: DatabaseDAO,
val spotifyService: SpotifyService,
val gaanaInterface : GaanaInterface, val gaanaInterface : GaanaInterface,
val ytDownloader: YoutubeDownloader val ytDownloader: YoutubeDownloader
) : ViewModel() { ) : ViewModel() {
var spotifyService = MutableStateFlow<SpotifyService?>(null) var isAuthenticated by mutableStateOf(false)
private set
private val _gradientColor = MutableStateFlow(colorPrimaryDark) fun authenticated(s:Boolean) {
val gradientColor : StateFlow<Color> isAuthenticated = s
get() = _gradientColor }
var gradientColor by mutableStateOf(colorPrimaryDark)
private set
fun updateGradientColor(color: Color) { fun updateGradientColor(color: Color) {
_gradientColor.value = color gradientColor = color
} }
fun resetGradient() { fun resetGradient() {
_gradientColor.value = colorPrimaryDark gradientColor = colorPrimaryDark
} }
} }

View File

@ -1,9 +1,12 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import com.shabinder.spotiflyer.models.spotify.Source
data class PlatformQueryResult( data class PlatformQueryResult(
var folderType: String, var folderType: String,
var subFolder: String, var subFolder: String,
var title: String, var title: String,
var coverUrl: String, var coverUrl: String,
var trackList: List<TrackDetails> var trackList: List<TrackDetails>,
var source: Source
) )

View File

@ -18,11 +18,12 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.Json
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Token( data class Token(
var access_token:String, var access_token:String?,
var token_type:String, var token_type:String?,
var expires_in:Int @Json(name = "expires_in") var expiry:Long?
): Parcelable ): Parcelable

View File

@ -7,12 +7,7 @@ import androidx.navigation.NavType
import androidx.navigation.compose.* import androidx.navigation.compose.*
import androidx.navigation.compose.popUpTo import androidx.navigation.compose.popUpTo
import com.shabinder.spotiflyer.ui.home.Home import com.shabinder.spotiflyer.ui.home.Home
import com.shabinder.spotiflyer.ui.platforms.gaana.Gaana import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.ui.platforms.spotify.Spotify
import com.shabinder.spotiflyer.ui.platforms.youtube.Youtube
import com.shabinder.spotiflyer.utils.mainActivity
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
@Composable @Composable
fun ComposeNavigation(navController: NavHostController) { fun ComposeNavigation(navController: NavHostController) {
@ -29,34 +24,10 @@ fun ComposeNavigation(navController: NavHostController) {
//Spotify Screen //Spotify Screen
//Argument `link` = Link of Track/Album/Playlist //Argument `link` = Link of Track/Album/Playlist
composable( composable(
"spotify/{link}", "track_list/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType }) arguments = listOf(navArgument("link") { type = NavType.StringType })
) { ) {
Spotify( TrackList(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Gaana Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"gaana/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Gaana(
fullLink = it.arguments?.getString("link") ?: "error",
navController = navController
)
}
//Youtube Screen
//Argument `link` = Link of Track/Album/Playlist
composable(
"youtube/{link}",
arguments = listOf(navArgument("link") { type = NavType.StringType })
) {
Youtube(
fullLink = it.arguments?.getString("link") ?: "error", fullLink = it.arguments?.getString("link") ?: "error",
navController = navController navController = navController
) )
@ -64,34 +35,10 @@ fun ComposeNavigation(navController: NavHostController) {
} }
} }
fun NavController.navigateToPlatform(link:String){ fun NavController.navigateToTrackList(link:String, singleInstance: Boolean = true, inclusive:Boolean = false) {
when{ navigate("track_list/$link") {
//SPOTIFY
link.contains("spotify",true) -> {
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
mainActivity.authenticateSpotify()
}
this.navigateAndPopUpToHome("spotify/$link")
}
//YOUTUBE
link.contains("youtube.com",true) || link.contains("youtu.be",true) -> {
this.navigateAndPopUpToHome("youtube/$link")
}
//GAANA
link.contains("gaana",true) -> {
this.navigateAndPopUpToHome("gaana/$link")
}
else -> showDialog("Link is Not Valid")
}
}
fun NavController.navigateAndPopUpToHome(route:String, inclusive:Boolean = false,singleInstance:Boolean = true){
this.navigate(route){
launchSingleTop = singleInstance launchSingleTop = singleInstance
popUpTo(route = "home"){ popUpTo(route = "home") {
this.inclusive = inclusive this.inclusive = inclusive
} }
} }

View File

@ -0,0 +1,40 @@
package com.shabinder.spotiflyer.networking
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.utils.TokenStore
import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.showDialog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Singleton
/**
* Interceptor which adds authorization token in header.
*/
@Singleton
class SpotifyAuthInterceptor @Inject constructor(
private val tokenStore: TokenStore,
) : Interceptor {
/*
* Local Copy for Token
* Live Throughout Session
* */
private var token by mutableStateOf<Token?>(null)
override fun intercept(chain: Interceptor.Chain): Response {
if(token?.expiry?:0 < System.currentTimeMillis()/1000){
//Token Expired time to fetch New One
runBlocking { token = tokenStore.getToken() }
log("Spotify Auth",token.toString())
}
val authRequest = chain.request().newBuilder().
addHeader("Authorization", "Bearer ${token?.access_token}").build()
return chain.proceed(authRequest)
}
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui.platforms.gaana package com.shabinder.spotiflyer.providers
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
@ -25,15 +25,39 @@ import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.gaana.GaanaTrack import com.shabinder.spotiflyer.models.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
private const val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" private const val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
suspend fun queryGaana(
fullLink: String,
):PlatformQueryResult?{
//Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")
val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
log("Gaana Fragment", "$type : $link")
//Error
if (type == "Error" || link == "Error"){
showDialog("Please Check Your Link!")
return null
}
return gaanaSearch(
type,
link,
sharedViewModel.gaanaInterface,
sharedViewModel.databaseDAO,
)
}
suspend fun gaanaSearch( suspend fun gaanaSearch(
type:String, type:String,
link:String, link:String,
@ -46,6 +70,7 @@ suspend fun gaanaSearch(
title = link, title = link,
coverUrl = gaanaPlaceholderImageUrl, coverUrl = gaanaPlaceholderImageUrl,
trackList = listOf(), trackList = listOf(),
Source.Gaana
) )
with(result) { with(result) {
when (type) { when (type) {

View File

@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui.platforms.spotify package com.shabinder.spotiflyer.providers
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.compose.runtime.Composable
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
@ -34,6 +35,40 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
suspend fun querySpotify(fullLink: String):PlatformQueryResult?{
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
if (!spotifyLink.contains("open.spotify")) {
//Very Rare instance
spotifyLink = resolveLink(spotifyLink, sharedViewModel.gaanaInterface)
}
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
log("Spotify Fragment", "$type : $link")
if (type == "Error" || link == "Error") {
showDialog("Please Check Your Link!")
return null
}
if (type == "episode" || type == "show") {
//TODO Implementation
showDialog("Implementing Soon, Stay Tuned!")
return null
}
return spotifySearch(
type,
link,
sharedViewModel.spotifyService,
sharedViewModel.databaseDAO
)
}
suspend fun spotifySearch( suspend fun spotifySearch(
type:String, type:String,
link: String, link: String,
@ -46,6 +81,7 @@ suspend fun spotifySearch(
title = "", title = "",
coverUrl = "", coverUrl = "",
trackList = listOf(), trackList = listOf(),
Source.Spotify
) )
with(result) { with(result) {
when (type) { when (type) {
@ -192,6 +228,10 @@ suspend fun spotifySearch(
return result return result
} }
/*
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
@WorkerThread @WorkerThread
fun resolveLink( fun resolveLink(
url:String, url:String,

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.ui.platforms.youtube package com.shabinder.spotiflyer.providers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
@ -37,6 +37,43 @@ import java.io.File
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg" * Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */ * */
private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com"
/*
* Sending a Result as null = Some Error Occurred!
* */
suspend fun queryYoutube(fullLink: String): PlatformQueryResult?{
val link = fullLink.removePrefix("https://").removePrefix("http://")
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
return getYTPlaylist(
playlistId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error")
}
if(link.contains(sampleDomain2,true) ){
searchId = link.substringAfterLast("/","error")
}
return if(searchId != "error") {
getYTTrack(
searchId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
null
}
}
}
suspend fun getYTPlaylist( suspend fun getYTPlaylist(
searchId: String, searchId: String,
ytDownloader: YoutubeDownloader, ytDownloader: YoutubeDownloader,
@ -48,6 +85,7 @@ suspend fun getYTPlaylist(
title = "", title = "",
coverUrl = "", coverUrl = "",
trackList = listOf(), trackList = listOf(),
Source.YouTube
) )
with(result) { with(result) {
try { try {
@ -127,6 +165,7 @@ suspend fun getYTTrack(
title = "", title = "",
coverUrl = "", coverUrl = "",
trackList = listOf(), trackList = listOf(),
Source.YouTube
) )
with(result) { with(result) {
try { try {

View File

@ -15,8 +15,6 @@ import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.* import androidx.compose.material.icons.rounded.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.viewinterop.viewModel import androidx.compose.ui.viewinterop.viewModel
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -34,14 +32,13 @@ import androidx.core.net.toUri
import androidx.navigation.NavController import androidx.navigation.NavController
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.navigation.navigateToPlatform import com.shabinder.spotiflyer.navigation.navigateToTrackList
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent import com.shabinder.spotiflyer.ui.colorAccent
import com.shabinder.spotiflyer.ui.colorPrimary import com.shabinder.spotiflyer.ui.colorPrimary
import com.shabinder.spotiflyer.utils.openPlatform import com.shabinder.spotiflyer.utils.openPlatform
import com.shabinder.spotiflyer.utils.sharedViewModel import com.shabinder.spotiflyer.utils.sharedViewModel
import dev.chrisbanes.accompanist.glide.GlideImage import dev.chrisbanes.accompanist.glide.GlideImage
import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun Home(navController: NavController, modifier: Modifier = Modifier) { fun Home(navController: NavController, modifier: Modifier = Modifier) {
@ -49,26 +46,23 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) {
Column(modifier = modifier) { Column(modifier = modifier) {
val link by viewModel.link.collectAsState() AuthenticationBanner(sharedViewModel.isAuthenticated,modifier)
AuthenticationBanner(viewModel,modifier)
SearchPanel( SearchPanel(
link, viewModel.link,
viewModel::updateLink, viewModel::updateLink,
navController, navController,
modifier modifier
) )
val selectedCategory by viewModel.selectedCategory.collectAsState()
HomeTabBar( HomeTabBar(
selectedCategory, viewModel.selectedCategory,
HomeCategory.values(), HomeCategory.values(),
viewModel::selectCategory, viewModel::selectCategory,
modifier modifier
) )
when(selectedCategory){ when(viewModel.selectedCategory){
HomeCategory.About -> AboutColumn() HomeCategory.About -> AboutColumn()
HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController) HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController)
} }
@ -212,11 +206,9 @@ fun AboutColumn(modifier: Modifier = Modifier) {
@Composable @Composable
fun HistoryColumn( fun HistoryColumn(
downloadRecordList: StateFlow<List<DownloadRecord>>, list: List<DownloadRecord>,
navController: NavController navController: NavController
) { ) {
val list by downloadRecordList.collectAsState()
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
content = { content = {
@ -254,16 +246,15 @@ fun DownloadRecordItem(item: DownloadRecord,navController: NavController) {
} }
Image( Image(
imageVector = vectorResource(id = R.drawable.ic_share_open), imageVector = vectorResource(id = R.drawable.ic_share_open),
modifier = Modifier.clickable(onClick = { navController.navigateToPlatform(item.link) }) modifier = Modifier.clickable(onClick = { navController.navigateToTrackList(item.link) })
) )
} }
} }
@Composable @Composable
fun AuthenticationBanner(viewModel: HomeViewModel, modifier: Modifier) { fun AuthenticationBanner(isAuthenticated: Boolean, modifier: Modifier) {
val authenticationStatus by viewModel.isAuthenticating.collectAsState()
if (authenticationStatus) { if (!isAuthenticated) {
// TODO show a progress indicator or similar // TODO show a progress indicator or similar
} }
} }
@ -321,7 +312,7 @@ fun SearchPanel(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(top = 16.dp,) modifier = modifier.padding(top = 16.dp)
){ ){
TextField( TextField(
leadingIcon = { leadingIcon = {
@ -346,7 +337,7 @@ fun SearchPanel(
OutlinedButton( OutlinedButton(
modifier = Modifier.padding(12.dp).wrapContentWidth(), modifier = Modifier.padding(12.dp).wrapContentWidth(),
onClick = { onClick = {
navController.navigateToPlatform(link) navController.navigateToTrackList(link)
}, },
border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent))) border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(colorPrimary, colorAccent)))
){ ){
@ -354,6 +345,8 @@ fun SearchPanel(
} }
} }
} }
@Composable @Composable
fun HomeCategoryTabIndicator( fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -2,56 +2,40 @@ package com.shabinder.spotiflyer.ui.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.utils.sharedViewModel import com.shabinder.spotiflyer.utils.sharedViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
private val _link = MutableStateFlow("") var link by mutableStateOf("")
val link:StateFlow<String> private set
get() = _link
fun updateLink(s:String) { fun updateLink(s:String) {
_link.value = s link = s
} }
private val _isAuthenticating = MutableStateFlow(true) var selectedCategory by mutableStateOf(HomeCategory.About)
val isAuthenticating:StateFlow<Boolean> private set
get() = _isAuthenticating
fun authenticated(s:Boolean) {
_isAuthenticating.value = s
}
private val _selectedCategory = MutableStateFlow(HomeCategory.About)
val selectedCategory :StateFlow<HomeCategory>
get() = _selectedCategory
fun selectCategory(s:HomeCategory) { fun selectCategory(s:HomeCategory) {
_selectedCategory.value = s selectedCategory = s
} }
private val _downloadRecordList = MutableStateFlow<List<DownloadRecord>>(listOf()) var downloadRecordList by mutableStateOf<List<DownloadRecord>>(listOf())
val downloadRecordList: StateFlow<List<DownloadRecord>>
get() = _downloadRecordList
fun getDownloadRecordList() { fun getDownloadRecordList() {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
_downloadRecordList.value = sharedViewModel.databaseDAO.getRecord().toMutableList() downloadRecordList = sharedViewModel.databaseDAO.getRecord()
} }
} }
} }
init {
getDownloadRecordList()
}
} }
enum class HomeCategory { enum class HomeCategory {

View File

@ -1,55 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.gaana
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch
@Composable
fun Gaana(
fullLink: String,
navController: NavController
) {
val source = Source.Gaana
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
//Coroutine Scope Active till this Composable is Active
val coroutineScope = rememberCoroutineScope()
//Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/")
val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
log("Gaana Fragment", "$type : $link")
//Error
if (type == "Error" || link == "Error"){
showDialog("Please Check Your Link!")
navController.popBackStack()
}
coroutineScope.launch {
result = gaanaSearch(
type,
link,
sharedViewModel.gaanaInterface,
sharedViewModel.databaseDAO,
)
}
result?.let {
TrackList(
result = it,
source = source
)
}
}

View File

@ -1,79 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.spotify
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun Spotify(fullLink: String, navController: NavController,) {
val source: Source = Source.Spotify
val coroutineScope = rememberCoroutineScope()
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
var spotifyLink =
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
log("Spotify Fragment Link", spotifyLink)
coroutineScope.launch(Dispatchers.Default) {
/*
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
if (!spotifyLink.contains("open.spotify")) {
val resolvedLink = resolveLink(spotifyLink, sharedViewModel.gaanaInterface)
log("Spotify Resolved Link", resolvedLink)
spotifyLink = resolvedLink
}
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
log("Spotify Fragment", "$type : $link")
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
if (isOnline()) mainActivity.authenticateSpotify()
}
if (type == "Error" || link == "Error") {
showDialog("Please Check Your Link!")
navController.popBackStack()
}
if (type == "episode" || type == "show") {
//TODO Implementation
showDialog("Implementing Soon, Stay Tuned!")
} else {
if (sharedViewModel.spotifyService.value == null){
//Authentication Still Pending
// TODO Better Implementation
showDialog("Authentication Failed")
navController.popBackStack()
}else{
result = spotifySearch(
type,
link,
sharedViewModel.spotifyService.value!!,
sharedViewModel.databaseDAO
)
}
}
}
result?.let {
log("Spotify",it.trackList.size.toString())
TrackList(
result = it,
source = source
)
}
}

View File

@ -1,64 +0,0 @@
package com.shabinder.spotiflyer.ui.platforms.youtube
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.NavController
import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import kotlinx.coroutines.launch
private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com"
@Composable
fun Youtube(fullLink: String, navController: NavController,) {
val source = Source.YouTube
var result by remember { mutableStateOf<PlatformQueryResult?>(null) }
//Coroutine Scope Active till this Composable is Active
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
val link = fullLink.removePrefix("https://").removePrefix("http://")
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
getYTPlaylist(
playlistId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error")
}
if(link.contains(sampleDomain2,true) ){
searchId = link.substringAfterLast("/","error")
}
if(searchId != "error") {
result = getYTTrack(
searchId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
navController.popBackStack()
}
}
}
result?.let {
TrackList(
result = it,
source = source
)
}
}

View File

@ -8,6 +8,10 @@ import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -19,14 +23,18 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.navigation.NavController
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.PlatformQueryResult import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.SpotiFlyerTypography import com.shabinder.spotiflyer.ui.SpotiFlyerTypography
import com.shabinder.spotiflyer.ui.colorAccent import com.shabinder.spotiflyer.ui.colorAccent
import com.shabinder.spotiflyer.providers.queryGaana
import com.shabinder.spotiflyer.providers.querySpotify
import com.shabinder.spotiflyer.providers.queryYoutube
import com.shabinder.spotiflyer.ui.utils.calculateDominantColor import com.shabinder.spotiflyer.ui.utils.calculateDominantColor
import com.shabinder.spotiflyer.utils.sharedViewModel import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog
import dev.chrisbanes.accompanist.coil.CoilImage import dev.chrisbanes.accompanist.coil.CoilImage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,19 +44,46 @@ import kotlinx.coroutines.launch
**/ **/
@Composable @Composable
fun TrackList( fun TrackList(
result: PlatformQueryResult, fullLink: String,
source: Source, navController: NavController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
){ ){
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var result by remember(fullLink) { mutableStateOf<PlatformQueryResult?>(null) }
coroutineScope.launch {
if(result == null){
result = when{
//SPOTIFY
fullLink.contains("spotify",true) -> querySpotify(fullLink)
//YOUTUBE
fullLink.contains("youtube.com",true) || fullLink.contains("youtu.be",true) -> queryYoutube(fullLink)
//GAANA
fullLink.contains("gaana",true) -> queryGaana(fullLink)
else -> {
showDialog("Link is Not Valid")
null
}
}
}
//Error Occurred And Has Been Shown to User
if(result == null) navController.popBackStack()
}
result?.let{
Box(modifier = modifier.fillMaxSize()){ Box(modifier = modifier.fillMaxSize()){
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
content = { content = {
item { item {
CoverImage(result.title,result.coverUrl,coroutineScope) CoverImage(it.title,it.coverUrl,coroutineScope)
} }
items(result.trackList) { items(it.trackList) {
TrackCard(track = it) TrackCard(track = it)
} }
}, },
@ -59,6 +94,7 @@ fun TrackList(
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
) )
} }
}
} }
@Composable @Composable
@ -87,7 +123,9 @@ fun CoverImage(
//color = colorAccent, //color = colorAccent,
) )
} }
scope.launch { updateGradient(coverURL) } scope.launch {
updateGradient(coverURL)
}
} }
@Composable @Composable

View File

@ -26,7 +26,8 @@ const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor { class NetworkInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
log("Network Requesting",chain.request().url.toString()) log("Network Requesting Debug",chain.request().url.toString())
return if (!isOnline()){ return if (!isOnline()){
//No Internet Connection //No Internet Connection
showDialog() showDialog()

View File

@ -3,15 +3,11 @@ package com.shabinder.spotiflyer.utils
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import android.util.Base64 import android.util.Base64
import androidx.lifecycle.ViewModelProvider
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.GaanaInterface import com.shabinder.spotiflyer.networking.*
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module import dagger.Module
@ -26,6 +22,7 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@ -59,15 +56,26 @@ object Provider {
@Provides @Provides
@Singleton @Singleton
fun getMoshi(): Moshi { fun getTokenStore(
return Moshi.Builder() @ApplicationContext appContext: Context,
.add(KotlinJsonAdapterFactory()) spotifyServiceTokenRequest: SpotifyServiceTokenRequest):TokenStore = TokenStore(appContext,spotifyServiceTokenRequest)
.build()
}
@Provides @Provides
@Singleton @Singleton
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest { fun getSpotifyService(authInterceptor: SpotifyAuthInterceptor,okHttpClient: OkHttpClient.Builder,moshi: Moshi) :SpotifyService{
val retrofit = Retrofit.Builder().run{
baseUrl("https://api.spotify.com/v1/")
client(okHttpClient.addInterceptor(authInterceptor).build())
addConverterFactory(MoshiConverterFactory.create(moshi))
build()
}
return retrofit.create(SpotifyService::class.java)
}
@Provides
@Singleton
fun getSpotifyTokenInterface(moshi: Moshi,networkInterceptor: NetworkInterceptor): SpotifyServiceTokenRequest {
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder() val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
.addInterceptor(Interceptor { chain -> .addInterceptor(Interceptor { chain ->
val request: Request = val request: Request =
@ -82,7 +90,7 @@ object Provider {
}" }"
).build() ).build()
chain.proceed(request) chain.proceed(request)
}).addInterceptor(NetworkInterceptor()) }).addInterceptor(networkInterceptor)
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://accounts.spotify.com/") .baseUrl("https://accounts.spotify.com/")
@ -94,18 +102,10 @@ object Provider {
@Provides @Provides
@Singleton @Singleton
fun okHttpClient(): OkHttpClient { fun getGaanaInterface(moshi: Moshi, okHttpClient: OkHttpClient.Builder): GaanaInterface {
return OkHttpClient.Builder()
.addInterceptor(NetworkInterceptor())
.build()
}
@Provides
@Singleton
fun getGaanaInterface(moshi: Moshi, okHttpClient: OkHttpClient): GaanaInterface {
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://api.gaana.com/") .baseUrl("https://api.gaana.com/")
.client(okHttpClient) .client(okHttpClient.build())
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(MoshiConverterFactory.create(moshi))
.build() .build()
return retrofit.create(GaanaInterface::class.java) return retrofit.create(GaanaInterface::class.java)
@ -122,4 +122,25 @@ object Provider {
return retrofit.create(YoutubeMusicApi::class.java) return retrofit.create(YoutubeMusicApi::class.java)
} }
@Provides
@Singleton
fun getNetworkInterceptor():NetworkInterceptor = NetworkInterceptor()
@Provides
@Singleton
fun okHttpClient(networkInterceptor: NetworkInterceptor): OkHttpClient.Builder {
return OkHttpClient.Builder()
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
.addInterceptor(networkInterceptor)
}
@Provides
@Singleton
fun getMoshi(): Moshi {
return Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
} }

View File

@ -0,0 +1,47 @@
package com.shabinder.spotiflyer.utils
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.createDataStore
import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class TokenStore (
context: Context,
private val spotifyServiceTokenRequest: SpotifyServiceTokenRequest
) {
private val dataStore = context.createDataStore(
name = "settings"
)
private val token = preferencesKey<String>("token")
private val tokenExpiry = preferencesKey<Long>("expiry")
suspend fun saveToken(tokenKey:String,time:Long){
dataStore.edit {
it[token] = tokenKey
it[tokenExpiry] = (System.currentTimeMillis()/1000) + time
}
}
suspend fun getToken(): Token?{
var token = dataStore.data.map {
Token(it[token],null,it[tokenExpiry])
}.firstOrNull()
if(System.currentTimeMillis()/1000 > token?.expiry?:0){
token = spotifyServiceTokenRequest.getToken().value
log("Spotify Token","Requesting New Token")
GlobalScope.launch { token?.access_token?.let { saveToken(it,token.expiry ?: 0) } }
}
return token
}
}