Modularisation, DI, and fixes

This commit is contained in:
shabinder 2021-01-03 03:16:29 +05:30
parent 86fd6a5123
commit 748a60adad
23 changed files with 868 additions and 751 deletions

View File

@ -15,8 +15,6 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -34,16 +32,16 @@ import com.github.javiersantos.appupdater.enums.Display
import com.github.javiersantos.appupdater.enums.UpdateFrom import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.razorpay.Checkout import com.razorpay.Checkout
import com.razorpay.PaymentResultListener import com.razorpay.PaymentResultListener
import com.shabinder.spotiflyer.di.Directories
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.navigation.ComposeNavigation import com.shabinder.spotiflyer.navigation.ComposeNavigation
import com.shabinder.spotiflyer.navigation.navigateToTrackList import com.shabinder.spotiflyer.navigation.navigateToTrackList
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.ui.ComposeLearnTheme import com.shabinder.spotiflyer.ui.ComposeLearnTheme
import com.shabinder.spotiflyer.ui.appNameStyle import com.shabinder.spotiflyer.ui.appNameStyle
import com.shabinder.spotiflyer.ui.colorOffWhite import com.shabinder.spotiflyer.ui.colorOffWhite
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.squareup.moshi.Moshi
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import dagger.hilt.EntryPoints
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
@ -54,13 +52,12 @@ import javax.inject.Inject
* This is App's God Activity * This is App's God Activity
* */ * */
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), PaymentResultListener { class MainActivity: AppCompatActivity(), PaymentResultListener {
private lateinit var navController: NavHostController private lateinit var navController: NavHostController
private lateinit var updateUIReceiver: BroadcastReceiver private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver private lateinit var queryReceiver: BroadcastReceiver
@Inject lateinit var moshi: Moshi @Inject lateinit var directories: Directories
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -72,15 +69,14 @@ class MainActivity : AppCompatActivity(), PaymentResultListener {
ComposeLearnTheme { ComposeLearnTheme {
Providers(AmbientContentColor provides colorOffWhite) { Providers(AmbientContentColor provides colorOffWhite) {
ProvideWindowInsets { ProvideWindowInsets {
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.7f) val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
navController = rememberNavController() navController = rememberNavController()
Column( Column(
modifier = Modifier.fillMaxSize().verticalGradientScrim( modifier = Modifier.fillMaxSize().verticalGradientScrim(
color = sharedViewModel.gradientColor.copy(alpha = 0.38f), color = sharedViewModel.gradientColor.copy(alpha = 0.38f),
startYPercentage = 1f, startYPercentage = 0.29f,
endYPercentage = 0f, endYPercentage = 0f,
fixedHeight = 700f,
) )
) { ) {
// Draw a scrim over the status bar which matches the app bar // Draw a scrim over the status bar which matches the app bar
@ -92,7 +88,13 @@ class MainActivity : AppCompatActivity(), PaymentResultListener {
backgroundColor = appBarColor, backgroundColor = appBarColor,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
ComposeNavigation(navController) ComposeNavigation(
this@MainActivity,
navController,
sharedViewModel.spotifyProvider,
sharedViewModel.gaanaProvider,
sharedViewModel.youtubeProvider,
)
} }
} }
} }
@ -106,7 +108,7 @@ class MainActivity : AppCompatActivity(), PaymentResultListener {
requestStoragePermission() requestStoragePermission()
disableDozeMode() disableDozeMode()
checkIfLatestVersion() checkIfLatestVersion()
createDirectories() createDirectories(directories.defaultDir(),directories.imageDir())
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
} }
@ -296,6 +298,7 @@ fun AppBar(
//@Preview(showBackground = true) //@Preview(showBackground = true)
/*
@Composable @Composable
fun DefaultPreview() { fun DefaultPreview() {
ComposeLearnTheme { ComposeLearnTheme {
@ -315,4 +318,4 @@ fun DefaultPreview() {
} }
} }
} }
} }*/

View File

@ -18,18 +18,22 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import android.content.Intent import android.content.Intent
import androidx.compose.runtime.* import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
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.ViewModel import androidx.lifecycle.ViewModel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
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.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
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.providers.GaanaProvider
import com.shabinder.spotiflyer.providers.SpotifyProvider
import com.shabinder.spotiflyer.providers.YoutubeProvider
import com.shabinder.spotiflyer.ui.colorPrimaryDark import com.shabinder.spotiflyer.ui.colorPrimaryDark
import com.shabinder.spotiflyer.utils.log import com.shabinder.spotiflyer.utils.log
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
@ -38,7 +42,10 @@ class SharedViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO, val databaseDAO: DatabaseDAO,
val spotifyService: SpotifyService, val spotifyService: SpotifyService,
val gaanaInterface : GaanaInterface, val gaanaInterface : GaanaInterface,
val ytDownloader: YoutubeDownloader val ytDownloader: YoutubeDownloader,
val gaanaProvider: GaanaProvider,
val spotifyProvider: SpotifyProvider,
val youtubeProvider: YoutubeProvider
) : ViewModel() { ) : ViewModel() {
var isAuthenticated by mutableStateOf(false) var isAuthenticated by mutableStateOf(false)
private set private set
@ -110,7 +117,7 @@ class SharedViewModel @ViewModelInject constructor(
} }
} }
var gradientColor by mutableStateOf(colorPrimaryDark) var gradientColor by mutableStateOf(Color.Transparent)
private set private set
fun updateGradientColor(color: Color) { fun updateGradientColor(color: Color) {

View File

@ -0,0 +1,13 @@
package com.shabinder.spotiflyer.di
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@EntryPoint
@InstallIn(SingletonComponent::class)
interface Directories {
@DefaultDir fun defaultDir():String
@ImageDir fun imageDir():String
}

View File

@ -1,19 +1,15 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.di
import android.content.Context
import android.os.Environment
import android.util.Base64 import android.util.Base64
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.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.* import com.shabinder.spotiflyer.networking.*
import com.shabinder.spotiflyer.utils.NetworkInterceptor
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
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -21,32 +17,13 @@ import okhttp3.Request
import retrofit2.Retrofit 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.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@Module @Module
object Provider { object NetworkUtilProvider {
//Default Directory to save Media in their Own Categorized Folders
@Suppress("DEPRECATION")// We Do Have Media Access (But Just Media in Media Directory,Not Anything Else)
val defaultDir = Environment.getExternalStorageDirectory().toString() + File.separator +
Environment.DIRECTORY_MUSIC + File.separator +
"SpotiFlyer"+ File.separator
//Default Cache Directory to save Album Art to use them for writing in Media Later
fun imageDir(ctx: Context = mainActivity): String = ctx
.externalCacheDir?.absolutePath + File.separator +
".Images" + File.separator
@Provides
@Singleton
fun databaseDAO(@ApplicationContext appContext: Context): DatabaseDAO {
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
}
@Provides @Provides
@Singleton @Singleton
@ -54,12 +31,6 @@ object Provider {
return YoutubeDownloader() return YoutubeDownloader()
} }
@Provides
@Singleton
fun getTokenStore(
@ApplicationContext appContext: Context,
spotifyServiceTokenRequest: SpotifyServiceTokenRequest):TokenStore = TokenStore(appContext,spotifyServiceTokenRequest)
@Provides @Provides
@Singleton @Singleton
fun getSpotifyService(authInterceptor: SpotifyAuthInterceptor,okHttpClient: OkHttpClient.Builder,moshi: Moshi) :SpotifyService{ fun getSpotifyService(authInterceptor: SpotifyAuthInterceptor,okHttpClient: OkHttpClient.Builder,moshi: Moshi) :SpotifyService{
@ -122,10 +93,6 @@ object Provider {
return retrofit.create(YoutubeMusicApi::class.java) return retrofit.create(YoutubeMusicApi::class.java)
} }
@Provides
@Singleton
fun getNetworkInterceptor():NetworkInterceptor = NetworkInterceptor()
@Provides @Provides
@Singleton @Singleton
fun okHttpClient(networkInterceptor: NetworkInterceptor): OkHttpClient.Builder { fun okHttpClient(networkInterceptor: NetworkInterceptor): OkHttpClient.Builder {

View File

@ -0,0 +1,67 @@
package com.shabinder.spotiflyer.di
import android.content.Context
import android.os.Environment
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.TokenStore
import dagger.Module
import dagger.Provides
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.io.File
import javax.inject.Qualifier
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
object UtilProvider {
//Default Cache Directory to save Album Art to use them for writing in Media Later
//Gets Cleaned After Service Destroy
@Provides
@Singleton
@ImageDir fun imageDir(@ApplicationContext appContext: Context):String =
appContext.cacheDir.absolutePath + File.separator
@Provides
@Singleton
@Suppress("DEPRECATION")
//Default Directory to save Media in their Own Categorized Folders
@DefaultDir fun defaultDir(@ApplicationContext appContext: Context):String =
appContext.externalMediaDirs[0].absolutePath +
File.separator
@Provides
@Singleton
fun databaseDAO(@ApplicationContext appContext: Context): DatabaseDAO {
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
}
@Provides
@Singleton
fun provideDirectories(@ApplicationContext appContext: Context):Directories = EntryPoints.get(appContext, Directories::class.java)
@Provides
@Singleton
fun getTokenStore(
@ApplicationContext appContext: Context,
spotifyServiceTokenRequest: SpotifyServiceTokenRequest
): TokenStore = TokenStore(appContext,spotifyServiceTokenRequest)
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDir
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ImageDir
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class FinalOutputDir

View File

@ -5,13 +5,22 @@ import androidx.navigation.NavController
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.* import androidx.navigation.compose.*
import androidx.navigation.compose.popUpTo import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.providers.GaanaProvider
import com.shabinder.spotiflyer.providers.SpotifyProvider
import com.shabinder.spotiflyer.providers.YoutubeProvider
import com.shabinder.spotiflyer.ui.home.Home import com.shabinder.spotiflyer.ui.home.Home
import com.shabinder.spotiflyer.ui.tracklist.TrackList import com.shabinder.spotiflyer.ui.tracklist.TrackList
import com.shabinder.spotiflyer.utils.sharedViewModel import com.shabinder.spotiflyer.utils.sharedViewModel
@Composable @Composable
fun ComposeNavigation(navController: NavHostController) { fun ComposeNavigation(
mainActivity: MainActivity,
navController: NavHostController,
spotifyProvider: SpotifyProvider,
gaanaProvider: GaanaProvider,
youtubeProvider: YoutubeProvider
) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "home" startDestination = "home"
@ -19,7 +28,7 @@ fun ComposeNavigation(navController: NavHostController) {
//HomeScreen - Starting Point //HomeScreen - Starting Point
composable("home") { composable("home") {
Home(navController = navController) Home(navController = navController, mainActivity)
} }
//Spotify Screen //Spotify Screen
@ -30,7 +39,10 @@ fun ComposeNavigation(navController: NavHostController) {
) { ) {
TrackList( TrackList(
fullLink = it.arguments?.getString("link") ?: "error", fullLink = it.arguments?.getString("link") ?: "error",
navController = navController navController = navController,
spotifyProvider,
gaanaProvider,
youtubeProvider
) )
} }
} }

View File

@ -6,8 +6,6 @@ import androidx.compose.runtime.setValue
import com.shabinder.spotiflyer.models.spotify.Token import com.shabinder.spotiflyer.models.spotify.Token
import com.shabinder.spotiflyer.utils.TokenStore import com.shabinder.spotiflyer.utils.TokenStore
import com.shabinder.spotiflyer.utils.log import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.showDialog
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response

View File

@ -17,217 +17,234 @@
package com.shabinder.spotiflyer.providers package com.shabinder.spotiflyer.providers
import android.content.Context
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.di.DefaultDir
import com.shabinder.spotiflyer.di.ImageDir
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
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.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.finalOutputDir
import com.shabinder.spotiflyer.utils.Provider.imageDir import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.queryActiveTracks
import com.shabinder.spotiflyer.utils.showDialog
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
private const val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" @Singleton
class GaanaProvider @Inject constructor(
@DefaultDir private val defaultDir: String,
@ImageDir private val imageDir: String,
private val gaanaInterface: GaanaInterface,
private val databaseDAO: DatabaseDAO,
@ApplicationContext private val ctx : Context
){
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
suspend fun queryGaana( suspend fun queryGaana(
fullLink: String, fullLink: String,
):PlatformQueryResult?{ ):PlatformQueryResult?{
//Link Schema: https://gaana.com/type/link //Link Schema: https://gaana.com/type/link
val gaanaLink = fullLink.substringAfter("gaana.com/") val gaanaLink = fullLink.substringAfter("gaana.com/")
val link = gaanaLink.substringAfterLast('/', "error") val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/') val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
log("Gaana Fragment", "$type : $link") log("Gaana Fragment", "$type : $link")
//Error //Error
if (type == "Error" || link == "Error"){ if (type == "Error" || link == "Error"){
showDialog("Please Check Your Link!") showDialog("Please Check Your Link!")
return null return null
}
return gaanaSearch(
type,
link,
sharedViewModel.gaanaInterface,
sharedViewModel.databaseDAO,
)
}
suspend fun gaanaSearch(
type:String,
link:String,
gaanaInterface: GaanaInterface,
databaseDAO: DatabaseDAO,
): PlatformQueryResult {
val result = PlatformQueryResult(
folderType = "",
subFolder = link,
title = link,
coverUrl = gaanaPlaceholderImageUrl,
trackList = listOf(),
Source.Gaana
)
with(result) {
when (type) {
"song" -> {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks"
subFolder = ""
if (File(
finalOutputDir(
it.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.track_title
coverUrl = it.artworkLink
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = 1,
)
)
}
}
}
"album" -> {
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
folderType = "Albums"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
title = link
coverUrl = it.custom_artworks.size_480p
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
"playlist" -> {
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
folderType = "Playlists"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
title = link
//coverUrl.value = "TODO"
coverUrl = gaanaPlaceholderImageUrl
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = it.tracks.size,
)
)
}
}
}
"artist" -> {
folderType = "Artist"
subFolder = link
coverUrl = gaanaPlaceholderImageUrl
val artistDetails =
gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()
?.also {
title = it.name
coverUrl = it.artworkLink ?: gaanaPlaceholderImageUrl
}
gaanaInterface.getGaanaArtistTracks(seokey = link).value?.also {
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Artist",
name = artistDetails?.name ?: link,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
else -> {//TODO Handle Error}
}
} }
queryActiveTracks() return gaanaSearch(
return result type,
link
)
}
private suspend fun gaanaSearch(
type:String,
link:String,
): PlatformQueryResult {
val result = PlatformQueryResult(
folderType = "",
subFolder = link,
title = link,
coverUrl = gaanaPlaceholderImageUrl,
trackList = listOf(),
Source.Gaana
)
with(result) {
when (type) {
"song" -> {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks"
subFolder = ""
if (File(
finalOutputDir(
it.track_title,
folderType,
subFolder,
defaultDir
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.track_title
coverUrl = it.artworkLink
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = 1,
)
)
}
}
}
"album" -> {
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
folderType = "Albums"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder,
defaultDir
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
title = link
coverUrl = it.custom_artworks.size_480p
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
"playlist" -> {
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
folderType = "Playlists"
subFolder = link
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder,
defaultDir
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
title = link
//coverUrl.value = "TODO"
coverUrl = gaanaPlaceholderImageUrl
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist",
name = title,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = it.tracks.size,
)
)
}
}
}
"artist" -> {
folderType = "Artist"
subFolder = link
coverUrl = gaanaPlaceholderImageUrl
val artistDetails =
gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()
?.also {
title = it.name
coverUrl = it.artworkLink ?: gaanaPlaceholderImageUrl
}
gaanaInterface.getGaanaArtistTracks(seokey = link).value?.also {
it.tracks.forEach { track ->
if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder,
defaultDir
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded
}
}
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Artist",
name = artistDetails?.name ?: link,
link = "https://gaana.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
else -> {//TODO Handle Error}
}
}
queryActiveTracks(ctx)
return result
}
}
private fun List<GaanaTrack>.toTrackDetailsList(type:String , subFolder:String) = this.map {
TrackDetails(
title = it.track_title,
artists = it.artist.map { artist -> artist?.name.toString() },
durationSec = it.duration,
albumArt = File(
imageDir + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
albumName = it.album_title,
year = it.release_date,
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana,
albumArtURL = it.artworkLink,
outputFile = finalOutputDir(it.track_title,type, subFolder,defaultDir,".m4a")
)
} }
} }
private fun List<GaanaTrack>.toTrackDetailsList(type:String , subFolder:String) = this.map {
TrackDetails(
title = it.track_title,
artists = it.artist.map { artist -> artist?.name.toString() },
durationSec = it.duration,
albumArt = File(
imageDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
albumName = it.album_title,
year = it.release_date,
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana,
albumArtURL = it.artworkLink,
outputFile = finalOutputDir(it.track_title,type, subFolder,".m4a")
)
}

View File

@ -17,10 +17,12 @@
package com.shabinder.spotiflyer.providers package com.shabinder.spotiflyer.providers
import androidx.annotation.WorkerThread import android.content.Context
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.di.DefaultDir
import com.shabinder.spotiflyer.di.Directories
import com.shabinder.spotiflyer.di.ImageDir
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.PlatformQueryResult import com.shabinder.spotiflyer.models.PlatformQueryResult
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
@ -30,232 +32,249 @@ import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track import com.shabinder.spotiflyer.models.spotify.Track
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.utils.* import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.queryActiveTracks
import com.shabinder.spotiflyer.utils.showDialog
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
suspend fun querySpotify(fullLink: String):PlatformQueryResult?{ @Singleton
var spotifyLink = class SpotifyProvider @Inject constructor(
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim() private val directories: Directories,
private val spotifyService: SpotifyService,
private val gaanaInterface: GaanaInterface,
private val databaseDAO: DatabaseDAO,
@ApplicationContext private val ctx : Context
) {
private val defaultDir
get() = directories.defaultDir()
private val imageDir
get() = directories.imageDir()
if (!spotifyLink.contains("open.spotify")) { suspend fun querySpotify(fullLink: String):PlatformQueryResult?{
//Very Rare instance var spotifyLink =
spotifyLink = resolveLink(spotifyLink, sharedViewModel.gaanaInterface) "https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
if (!spotifyLink.contains("open.spotify")) {
//Very Rare instance
spotifyLink = resolveLink(spotifyLink)
}
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
)
} }
private suspend fun spotifySearch(
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') type:String,
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') link: String
): PlatformQueryResult {
log("Spotify Fragment", "$type : $link") val result = PlatformQueryResult(
folderType = "",
if (type == "Error" || link == "Error") { subFolder = "",
showDialog("Please Check Your Link!") title = "",
return null coverUrl = "",
} trackList = listOf(),
Source.Spotify
if (type == "episode" || type == "show") { )
//TODO Implementation with(result) {
showDialog("Implementing Soon, Stay Tuned!") when (type) {
return null "track" -> {
} spotifyService.getTrack(link).value?.also {
folderType = "Tracks"
return spotifySearch( subFolder = ""
type,
link,
sharedViewModel.spotifyService,
sharedViewModel.databaseDAO
)
}
suspend fun spotifySearch(
type:String,
link: String,
spotifyService: SpotifyService,
databaseDAO: DatabaseDAO
): PlatformQueryResult {
val result = PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.Spotify
)
with(result) {
when (type) {
"track" -> {
spotifyService.getTrack(link).value?.also {
folderType = "Tracks"
subFolder = ""
if (File(
finalOutputDir(
it.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.name.toString()
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title,
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl,
totalFiles = 1,
)
)
}
}
}
"album" -> {
val albumObject = spotifyService.getAlbum(link).value
folderType = "Albums"
subFolder = albumObject?.name.toString()
albumObject?.tracks?.items?.forEach {
if (File(
finalOutputDir(
it.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded
}
it.album = Album(
images = listOf(
Image(
url = albumObject.images?.elementAtOrNull(1)?.url
?: albumObject.images?.elementAtOrNull(0)?.url
)
)
)
}
albumObject?.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {
if (it.isNullOrEmpty()) {
//TODO Handle Error
showDialog("Error Fetching Album")
} else {
trackList = it
title = albumObject?.name.toString()
coverUrl = (albumObject?.images?.elementAtOrNull(1)?.url
?: albumObject?.images?.elementAtOrNull(0)?.url).toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title,
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
}
"playlist" -> {
log("Spotify Service", spotifyService.toString())
val playlistObject = spotifyService.getPlaylist(link).value
folderType = "Playlists"
subFolder = playlistObject?.name.toString()
val tempTrackList = mutableListOf<Track>()
log("Tracks Fetched", playlistObject?.tracks?.items?.size.toString())
playlistObject?.tracks?.items?.forEach {
it.track?.let { it1 ->
if (File( if (File(
finalOutputDir( finalOutputDir(
it1.name.toString(), it.name.toString(),
folderType, folderType,
subFolder subFolder,
defaultDir
) )
).exists() ).exists()
) {//Download Already Present!! ) {//Download Already Present!!
it1.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
}
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
title = it.name.toString()
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = title,
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl,
totalFiles = 1,
)
)
} }
tempTrackList.add(it1)
} }
} }
var moreTracksAvailable = !playlistObject?.tracks?.next.isNullOrBlank()
while (moreTracksAvailable) { "album" -> {
//Check For More Tracks If available val albumObject = spotifyService.getAlbum(link).value
val moreTracks = folderType = "Albums"
spotifyService.getPlaylistTracks(link, offset = tempTrackList.size).value subFolder = albumObject?.name.toString()
moreTracks?.items?.forEach { albumObject?.tracks?.items?.forEach {
it.track?.let { it1 -> tempTrackList.add(it1) } if (File(
} finalOutputDir(
moreTracksAvailable = !moreTracks?.next.isNullOrBlank() it.name.toString(),
} folderType,
log("Total Tracks Fetched", tempTrackList.size.toString()) subFolder,
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder) defaultDir
title = playlistObject?.name.toString() )
coverUrl = playlistObject?.images?.elementAtOrNull(1)?.url ).exists()
?: playlistObject?.images?.firstOrNull()?.url.toString() ) {//Download Already Present!!
withContext(Dispatchers.IO) { it.downloaded = DownloadStatus.Downloaded
databaseDAO.insert( }
DownloadRecord( it.album = Album(
type = "Playlist", images = listOf(
name = title, Image(
link = "https://open.spotify.com/$type/$link", url = albumObject.images?.elementAtOrNull(1)?.url
coverUrl = coverUrl, ?: albumObject.images?.elementAtOrNull(0)?.url
totalFiles = tempTrackList.size, )
)
) )
) }
albumObject?.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {
if (it.isNullOrEmpty()) {
//TODO Handle Error
showDialog("Error Fetching Album")
} else {
trackList = it
title = albumObject?.name.toString()
coverUrl = (albumObject?.images?.elementAtOrNull(1)?.url
?: albumObject?.images?.elementAtOrNull(0)?.url).toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album",
name = title,
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl,
totalFiles = trackList.size,
)
)
}
}
}
}
"playlist" -> {
log("Spotify Service", spotifyService.toString())
val playlistObject = spotifyService.getPlaylist(link).value
folderType = "Playlists"
subFolder = playlistObject?.name.toString()
val tempTrackList = mutableListOf<Track>()
log("Tracks Fetched", playlistObject?.tracks?.items?.size.toString())
playlistObject?.tracks?.items?.forEach {
it.track?.let { it1 ->
if (File(
finalOutputDir(
it1.name.toString(),
folderType,
subFolder,
defaultDir
)
).exists()
) {//Download Already Present!!
it1.downloaded = DownloadStatus.Downloaded
}
tempTrackList.add(it1)
}
}
var moreTracksAvailable = !playlistObject?.tracks?.next.isNullOrBlank()
while (moreTracksAvailable) {
//Check For More Tracks If available
val moreTracks =
spotifyService.getPlaylistTracks(link, offset = tempTrackList.size).value
moreTracks?.items?.forEach {
it.track?.let { it1 -> tempTrackList.add(it1) }
}
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
}
log("Total Tracks Fetched", tempTrackList.size.toString())
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder)
title = playlistObject?.name.toString()
coverUrl = playlistObject?.images?.elementAtOrNull(1)?.url
?: playlistObject?.images?.firstOrNull()?.url.toString()
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist",
name = title,
link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl,
totalFiles = tempTrackList.size,
)
)
}
}
"episode" -> {//TODO
}
"show" -> {//TODO
}
else -> {
//TODO Handle Error
} }
}
"episode" -> {//TODO
}
"show" -> {//TODO
}
else -> {
//TODO Handle Error
} }
} }
queryActiveTracks(ctx)
return result
} }
queryActiveTracks()
return result
}
/* /*
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb, * New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630 * Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */ * */
@WorkerThread private fun resolveLink(
fun resolveLink( url:String
url:String, ):String {
gaanaInterface: GaanaInterface val response = gaanaInterface.getResponse(url).execute().body()?.string().toString()
):String { val regex = """https://open\.spotify\.com.+\w""".toRegex()
val response = gaanaInterface.getResponse(url).execute().body()?.string().toString() return regex.find(response)?.value.toString()
val regex = """https://open\.spotify\.com.+\w""".toRegex() }
return regex.find(response)?.value.toString()
}
private fun List<Track>.toTrackDetailsList(type:String, subFolder:String) = this.map { private fun List<Track>.toTrackDetailsList(type:String, subFolder:String) = this.map {
TrackDetails( TrackDetails(
title = it.name.toString(), title = it.name.toString(),
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
durationSec = (it.duration_ms/1000).toInt(), durationSec = (it.duration_ms/1000).toInt(),
albumArt = File( albumArt = File(
Provider.imageDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"), imageDir + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name, albumName = it.album?.name,
year = it.album?.release_date, year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}", comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href, trackUrl = it.href,
downloaded = it.downloaded, downloaded = it.downloaded,
source = Source.Spotify, source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(), albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFile = finalOutputDir(it.name.toString(),type, subFolder,".m4a") outputFile = finalOutputDir(it.name.toString(),type, subFolder,defaultDir,".m4a")
) )
}
} }

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.downloadHelper package com.shabinder.spotiflyer.providers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import com.beust.klaxon.JsonArray import com.beust.klaxon.JsonArray

View File

@ -18,214 +18,224 @@
package com.shabinder.spotiflyer.providers package com.shabinder.spotiflyer.providers
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
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.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.di.DefaultDir
import com.shabinder.spotiflyer.di.Directories
import com.shabinder.spotiflyer.di.ImageDir
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
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.models.spotify.Source
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
/* @Singleton
* YT Album Art Schema class YoutubeProvider @Inject constructor(
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" private val directories: Directories,
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg" private val ytDownloader: YoutubeDownloader,
* */ private val databaseDAO: DatabaseDAO,
@ApplicationContext private val ctx : Context,
) {
/*
* YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */
private val sampleDomain2 = "youtu.be"
private val sampleDomain1 = "youtube.com"
private const val sampleDomain2 = "youtu.be" private val defaultDir
private const val sampleDomain1 = "youtube.com" get() = directories.defaultDir()
private val imageDir
get() = directories.imageDir()
/* /*
* Sending a Result as null = Some Error Occurred! * Sending a Result as null = Some Error Occurred!
* */ * */
suspend fun queryYoutube(fullLink: String): PlatformQueryResult?{ suspend fun queryYoutube(fullLink: String): PlatformQueryResult?{
val link = fullLink.removePrefix("https://").removePrefix("http://") val link = fullLink.removePrefix("https://").removePrefix("http://")
if(link.contains("playlist",true) || link.contains("list",true)){ if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist // Given Link is of a Playlist
log("YT Play",link) log("YT Play",link)
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&") val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
return getYTPlaylist( return getYTPlaylist(
playlistId, 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
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
null
}
}
}
private suspend fun getYTPlaylist(
searchId: String
):PlatformQueryResult?{
val result = PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
) )
}else{//Given Link is of a Video with(result) {
var searchId = "error" try {
if(link.contains(sampleDomain1,true) ){ log("YT Playlist", searchId)
searchId = link.substringAfterLast("=","error") val playlist = ytDownloader.getPlaylist(searchId)
} val playlistDetails = playlist.details()
if(link.contains(sampleDomain2,true) ){ val name = playlistDetails.title()
searchId = link.substringAfterLast("/","error") subFolder = removeIllegalChars(name)
} val videos = playlist.videos()
return if(searchId != "error") {
getYTTrack(
searchId,
sharedViewModel.ytDownloader,
sharedViewModel.databaseDAO
)
}else{
showDialog("Your Youtube Link is not of a Video!!")
null
}
}
}
suspend fun getYTPlaylist( coverUrl = "https://i.ytimg.com/vi/${
searchId: String, videos.firstOrNull()?.videoId()
ytDownloader: YoutubeDownloader, }/hqdefault.jpg"
databaseDAO: DatabaseDAO, title = name
):PlatformQueryResult?{
val result = PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
)
with(result) {
try {
log("YT Playlist", searchId)
val playlist = ytDownloader.getPlaylist(searchId)
val playlistDetails = playlist.details()
val name = playlistDetails.title()
subFolder = removeIllegalChars(name)
val videos = playlist.videos()
coverUrl = "https://i.ytimg.com/vi/${ trackList = videos.map {
videos.firstOrNull()?.videoId() TrackDetails(
}/hqdefault.jpg" title = it.title(),
title = name artists = listOf(it.author().toString()),
durationSec = it.lengthSeconds(),
trackList = videos.map { albumArt = File(imageDir + it.videoId() + ".jpeg"),
TrackDetails( source = Source.YouTube,
title = it.title(), albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
artists = listOf(it.author().toString()), downloaded = if (File(
durationSec = it.lengthSeconds(), finalOutputDir(
albumArt = File( itemName = it.title(),
imageDir() + it.videoId() + ".jpeg" type = folderType,
), subFolder = subFolder,
source = Source.YouTube, defaultDir
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg", )
downloaded = if (File( ).exists()
finalOutputDir( )
itemName = it.title(), DownloadStatus.Downloaded
type = folderType, else {
subFolder = subFolder DownloadStatus.NotDownloaded
)
).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(it.title(), folderType, subFolder, ".m4a"),
videoID = it.videoId()
)
}
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "PlayList",
name = if (name.length > 17) {
"${name.subSequence(0, 16)}..."
} else {
name
}, },
link = "https://www.youtube.com/playlist?list=$searchId", outputFile = finalOutputDir(it.title(), folderType, subFolder, defaultDir,".m4a"),
coverUrl = "https://i.ytimg.com/vi/${ videoID = it.videoId()
videos.firstOrNull()?.videoId()
}/hqdefault.jpg",
totalFiles = videos.size,
) )
) }
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "PlayList",
name = if (name.length > 17) {
"${name.subSequence(0, 16)}..."
} else {
name
},
link = "https://www.youtube.com/playlist?list=$searchId",
coverUrl = "https://i.ytimg.com/vi/${
videos.firstOrNull()?.videoId()
}/hqdefault.jpg",
totalFiles = videos.size,
)
)
}
queryActiveTracks(ctx)
} catch (e: Exception) {
e.printStackTrace()
showDialog("An Error Occurred While Processing!")
} }
queryActiveTracks()
} catch (e: Exception) {
e.printStackTrace()
showDialog("An Error Occurred While Processing!")
} }
} return if(result.title.isNotBlank()) result
return if(result.title.isNotBlank()) result
else null else null
}
@SuppressLint("DefaultLocale")
suspend fun getYTTrack(
searchId:String,
ytDownloader: YoutubeDownloader,
databaseDAO: DatabaseDAO
):PlatformQueryResult? {
val result = PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
)
with(result) {
try {
log("YT Video", searchId)
val video = ytDownloader.getVideo(searchId)
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
val detail = video?.details()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
?: detail?.title() ?: ""
log("YT View Model", detail.toString())
trackList = listOf(
TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds() ?: 0,
albumArt = File(imageDir(), "$searchId.jpeg"),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder
)
).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(name, folderType, subFolder, ".m4a"),
videoID = searchId
)
)
title = name
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = if (name.length > 17) {
"${name.subSequence(0, 16)}..."
} else {
name
},
link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1,
)
)
}
queryActiveTracks()
} catch (e: Exception) {
e.printStackTrace()
showDialog("An Error Occurred While Processing!")
}
} }
return if(result.title.isNotBlank()) result
else null @SuppressLint("DefaultLocale")
} private suspend fun getYTTrack(
searchId:String,
):PlatformQueryResult? {
val result = PlatformQueryResult(
folderType = "",
subFolder = "",
title = "",
coverUrl = "",
trackList = listOf(),
Source.YouTube
)
with(result) {
try {
log("YT Video", searchId)
val video = ytDownloader.getVideo(searchId)
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
val detail = video?.details()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
?: detail?.title() ?: ""
log("YT View Model", detail.toString())
trackList = listOf(
TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds() ?: 0,
albumArt = File(imageDir, "$searchId.jpeg"),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder,
defaultDir = defaultDir
)
).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(name, folderType, subFolder, defaultDir,".m4a"),
videoID = searchId
)
)
title = name
withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track",
name = if (name.length > 17) {
"${name.subSequence(0, 16)}..."
} else {
name
},
link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1,
)
)
}
queryActiveTracks(ctx)
} catch (e: Exception) {
e.printStackTrace()
showDialog("An Error Occurred While Processing!")
}
}
return if(result.title.isNotBlank()) result
else null
}
}

View File

@ -1,10 +1,11 @@
package com.shabinder.spotiflyer.ui package com.shabinder.spotiflyer.ui
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Typography import androidx.compose.material.Typography
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.font
import androidx.compose.ui.text.font.fontFamily
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R

View File

@ -14,14 +14,17 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.History 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.CardGiftcard
import androidx.compose.material.icons.rounded.Flag
import androidx.compose.material.icons.rounded.InsertLink
import androidx.compose.material.icons.rounded.Share
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.AmbientContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@ -29,21 +32,26 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow 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.compose.ui.viewinterop.viewModel
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.navigation.NavController import androidx.navigation.NavController
import com.razorpay.Checkout import com.razorpay.Checkout
import com.shabinder.spotiflyer.MainActivity
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.navigateToTrackList 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.* import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.openPlatform
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 org.json.JSONObject import org.json.JSONObject
@Composable @Composable
fun Home(navController: NavController, modifier: Modifier = Modifier) { fun Home(navController: NavController, mainActivity: MainActivity, modifier: Modifier = Modifier) {
val viewModel: HomeViewModel = viewModel() val viewModel: HomeViewModel = viewModel()
Column(modifier = modifier) { Column(modifier = modifier) {
@ -65,7 +73,7 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) {
) )
when(viewModel.selectedCategory){ when(viewModel.selectedCategory){
HomeCategory.About -> AboutColumn() HomeCategory.About -> AboutColumn(mainActivity)
HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController) HomeCategory.History -> HistoryColumn(viewModel.downloadRecordList,navController)
} }
} }
@ -77,7 +85,8 @@ fun Home(navController: NavController, modifier: Modifier = Modifier) {
@Composable @Composable
fun AboutColumn(modifier: Modifier = Modifier) { fun AboutColumn(mainActivity: MainActivity,modifier: Modifier = Modifier) {
val ctx = AmbientContext.current
ScrollableColumn(modifier.fillMaxSize(),contentPadding = PaddingValues(16.dp)) { ScrollableColumn(modifier.fillMaxSize(),contentPadding = PaddingValues(16.dp)) {
Card( Card(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@ -94,17 +103,17 @@ fun AboutColumn(modifier: Modifier = Modifier) {
Icon( Icon(
imageVector = vectorResource(id = R.drawable.ic_spotify_logo), tint = Color.Unspecified, imageVector = vectorResource(id = R.drawable.ic_spotify_logo), tint = Color.Unspecified,
modifier = Modifier.clickable( modifier = Modifier.clickable(
onClick = { openPlatform("com.spotify.music","http://open.spotify.com") }) onClick = { openPlatform("com.spotify.music","http://open.spotify.com",ctx) })
) )
Spacer(modifier = modifier.padding(start = 24.dp)) Spacer(modifier = modifier.padding(start = 24.dp))
Icon(imageVector = vectorResource(id = R.drawable.ic_gaana ),tint = Color.Unspecified, Icon(imageVector = vectorResource(id = R.drawable.ic_gaana ),tint = Color.Unspecified,
modifier = Modifier.clickable( modifier = Modifier.clickable(
onClick = { openPlatform("com.gaana","http://gaana.com") }) onClick = { openPlatform("com.gaana","http://gaana.com",ctx) })
) )
Spacer(modifier = modifier.padding(start = 24.dp)) Spacer(modifier = modifier.padding(start = 24.dp))
Icon(imageVector = vectorResource(id = R.drawable.ic_youtube),tint = Color.Unspecified, Icon(imageVector = vectorResource(id = R.drawable.ic_youtube),tint = Color.Unspecified,
modifier = Modifier.clickable( modifier = Modifier.clickable(
onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com") }) onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com",ctx) })
) )
} }
} }
@ -123,7 +132,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.padding(top = 6.dp)) Spacer(modifier = Modifier.padding(top = 6.dp))
Row(verticalAlignment = Alignment.CenterVertically, Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable( modifier = Modifier.fillMaxWidth().clickable(
onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer") }) onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer",ctx) })
.padding(vertical = 6.dp) .padding(vertical = 6.dp)
) { ) {
Icon(imageVector = vectorResource(id = R.drawable.ic_github ),tint = Color.LightGray) Icon(imageVector = vectorResource(id = R.drawable.ic_github ),tint = Color.LightGray)
@ -141,7 +150,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
} }
Row( Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer") }), .clickable(onClick = { openPlatform("http://github.com/Shabinder/SpotiFlyer", ctx) }),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Rounded.Flag.copy(defaultHeight = 32.dp,defaultWidth = 32.dp)) Icon(Icons.Rounded.Flag.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
@ -159,7 +168,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
} }
Row( Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { startPayment() }), .clickable(onClick = { startPayment(mainActivity) }),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Rounded.CardGiftcard.copy(defaultHeight = 32.dp,defaultWidth = 32.dp)) Icon(Icons.Rounded.CardGiftcard.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
@ -185,7 +194,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
} }
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
mainActivity.startActivity(shareIntent) ctx.startActivity(shareIntent)
}), }),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -245,6 +254,7 @@ fun HistoryColumn(
@Composable @Composable
fun DownloadRecordItem(item: DownloadRecord,navController: NavController) { fun DownloadRecordItem(item: DownloadRecord,navController: NavController) {
val ctx = AmbientContext.current
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) { Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
val imgUri = item.coverUrl.toUri().buildUpon().scheme("https").build() val imgUri = item.coverUrl.toUri().buildUpon().scheme("https").build()
CoilImage( CoilImage(
@ -270,14 +280,14 @@ 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 = { modifier = Modifier.clickable(onClick = {
if(!isOnline()) showDialog("Check Your Internet Connection") if(!isOnline(ctx)) showDialog("Check Your Internet Connection")
else navController.navigateToTrackList(item.link) else navController.navigateToTrackList(item.link)
}) })
) )
} }
} }
private fun startPayment() { private fun startPayment(mainActivity: MainActivity) {
/* /*
* You need to pass current activity in order to let Razorpay create CheckoutActivity * You need to pass current activity in order to let Razorpay create CheckoutActivity
* */ * */
@ -365,7 +375,7 @@ fun SearchPanel(
navController: NavController, navController: NavController,
modifier: Modifier = Modifier modifier: Modifier = Modifier
){ ){
val ctx = AmbientContext.current
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(top = 16.dp) modifier = modifier.padding(top = 16.dp)
@ -395,7 +405,7 @@ fun SearchPanel(
onClick = { onClick = {
if(link.isBlank()) showDialog("Enter A Link!") if(link.isBlank()) showDialog("Enter A Link!")
else{ else{
if(!isOnline()) showDialog("Check Your Internet Connection") if(!isOnline(ctx)) showDialog("Check Your Internet Connection")
else navController.navigateToTrackList(link) else navController.navigateToTrackList(link)
} }
}, },

View File

@ -1,16 +1,16 @@
package com.shabinder.spotiflyer.ui.home package com.shabinder.spotiflyer.ui.home
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
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.delay
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
import kotlinx.coroutines.delay
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {

View File

@ -1,9 +1,7 @@
package com.shabinder.spotiflyer.ui.tracklist package com.shabinder.spotiflyer.ui.tracklist
import android.content.Context
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -14,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.AmbientContext
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -25,14 +24,13 @@ import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
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.providers.GaanaProvider
import com.shabinder.spotiflyer.providers.SpotifyProvider
import com.shabinder.spotiflyer.providers.YoutubeProvider
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.downloadTracks import com.shabinder.spotiflyer.utils.downloadTracks
import com.shabinder.spotiflyer.utils.log
import com.shabinder.spotiflyer.utils.sharedViewModel import com.shabinder.spotiflyer.utils.sharedViewModel
import com.shabinder.spotiflyer.utils.showDialog import com.shabinder.spotiflyer.utils.showDialog
import dev.chrisbanes.accompanist.coil.CoilImage import dev.chrisbanes.accompanist.coil.CoilImage
@ -45,6 +43,9 @@ import kotlinx.coroutines.*
fun TrackList( fun TrackList(
fullLink: String, fullLink: String,
navController: NavController, navController: NavController,
spotifyProvider: SpotifyProvider,
gaanaProvider: GaanaProvider,
youtubeProvider: YoutubeProvider,
modifier: Modifier = Modifier modifier: Modifier = Modifier
){ ){
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -60,13 +61,16 @@ fun TrackList(
* Using SharedViewModel's Link as NAVIGATION's Arg is buggy for links. * Using SharedViewModel's Link as NAVIGATION's Arg is buggy for links.
* */ * */
//SPOTIFY //SPOTIFY
sharedViewModel.link.contains("spotify",true) -> querySpotify(sharedViewModel.link) sharedViewModel.link.contains("spotify",true) ->
spotifyProvider.querySpotify(sharedViewModel.link)
//YOUTUBE //YOUTUBE
sharedViewModel.link.contains("youtube.com",true) || sharedViewModel.link.contains("youtu.be",true) -> queryYoutube(sharedViewModel.link) sharedViewModel.link.contains("youtube.com",true) || sharedViewModel.link.contains("youtu.be",true) ->
youtubeProvider.queryYoutube(sharedViewModel.link)
//GAANA //GAANA
sharedViewModel.link.contains("gaana",true) -> queryGaana(sharedViewModel.link) sharedViewModel.link.contains("gaana",true) ->
gaanaProvider.queryGaana(sharedViewModel.link)
else -> { else -> {
showDialog("Link is Not Valid") showDialog("Link is Not Valid")
@ -83,6 +87,7 @@ fun TrackList(
sharedViewModel.updateTrackList(result?.trackList ?: listOf()) sharedViewModel.updateTrackList(result?.trackList ?: listOf())
result?.let{ result?.let{
val ctx = AmbientContext.current
Box(modifier = modifier.fillMaxSize()){ Box(modifier = modifier.fillMaxSize()){
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@ -94,7 +99,7 @@ fun TrackList(
TrackCard( TrackCard(
track = item, track = item,
onDownload = { onDownload = {
downloadTracks(arrayListOf(item)) downloadTracks(arrayListOf(item),ctx)
sharedViewModel.updateTrackStatus(index,DownloadStatus.Queued) sharedViewModel.updateTrackStatus(index,DownloadStatus.Queued)
}, },
) )
@ -106,7 +111,7 @@ fun TrackList(
onClick = { onClick = {
val finalList = sharedViewModel.trackList.filter{it.downloaded == DownloadStatus.NotDownloaded} val finalList = sharedViewModel.trackList.filter{it.downloaded == DownloadStatus.NotDownloaded}
if (finalList.isNullOrEmpty()) showDialog("Not Downloading Any Song") if (finalList.isNullOrEmpty()) showDialog("Not Downloading Any Song")
else downloadTracks(finalList as ArrayList<TrackDetails>) else downloadTracks(finalList as ArrayList<TrackDetails>,ctx)
for (track in sharedViewModel.trackList) { for (track in sharedViewModel.trackList) {
if (track.downloaded == DownloadStatus.NotDownloaded) { if (track.downloaded == DownloadStatus.NotDownloaded) {
track.downloaded = DownloadStatus.Queued track.downloaded = DownloadStatus.Queued
@ -126,12 +131,14 @@ fun CoverImage(
scope: CoroutineScope, scope: CoroutineScope,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val ctx = AmbientContext.current
Column( Column(
modifier.padding(vertical = 8.dp).fillMaxWidth(), modifier.padding(vertical = 8.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val imgUri = coverURL.toUri().buildUpon().scheme("https").build()
CoilImage( CoilImage(
data = coverURL, data = imgUri,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
loading = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) }, loading = { Image(vectorResource(id = R.drawable.ic_musicplaceholder)) },
modifier = Modifier modifier = Modifier
@ -149,7 +156,7 @@ fun CoverImage(
) )
} }
scope.launch { scope.launch {
updateGradient(coverURL) updateGradient(coverURL, ctx)
} }
} }
@ -187,8 +194,8 @@ fun TrackCard(
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize() modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
){ ){
Text("${track.artists.firstOrNull()}...",fontSize = 13.sp) Text("${track.artists.firstOrNull()}...",fontSize = 12.sp,maxLines = 1)
Text("${track.durationSec/60} minutes, ${track.durationSec%60} sec",fontSize = 13.sp) Text("${track.durationSec/60} min, ${track.durationSec%60} sec",fontSize = 12.sp,maxLines = 1,overflow = TextOverflow.Ellipsis)
} }
} }
when(track.downloaded){ when(track.downloaded){
@ -216,7 +223,7 @@ fun TrackCard(
} }
} }
suspend fun updateGradient(imageURL:String){ suspend fun updateGradient(imageURL:String,ctx:Context){
calculateDominantColor(imageURL)?.color calculateDominantColor(imageURL,ctx)?.color
?.let { sharedViewModel.updateGradientColor(it) } ?.let { sharedViewModel.updateGradientColor(it) }
} }

View File

@ -17,27 +17,14 @@
package com.shabinder.spotiflyer.ui.utils package com.shabinder.spotiflyer.ui.utils
import android.content.Context import android.content.Context
import androidx.collection.LruCache
import androidx.compose.animation.animate
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AmbientContext
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.palette.graphics.Palette import androidx.palette.graphics.Palette
import coil.Coil import coil.Coil
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.SuccessResult import coil.request.SuccessResult
import coil.size.Scale import coil.size.Scale
import com.shabinder.spotiflyer.utils.mainActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -45,9 +32,9 @@ import kotlinx.coroutines.withContext
data class DominantColors(val color: Color, val onColor: Color) data class DominantColors(val color: Color, val onColor: Color)
suspend fun calculateDominantColor(url: String): DominantColors? { suspend fun calculateDominantColor(url: String,ctx:Context): DominantColors? {
// we calculate the swatches in the image, and return the first valid color // we calculate the swatches in the image, and return the first valid color
return calculateSwatchesInImage(mainActivity, url) return calculateSwatchesInImage(ctx, url)
// First we want to sort the list by the color's population // First we want to sort the list by the color's population
.sortedByDescending { swatch -> swatch.population } .sortedByDescending { swatch -> swatch.population }
// Then we want to find the first valid color // Then we want to find the first valid color

View File

@ -26,7 +26,6 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.shabinder.spotiflyer.utils.log
import kotlin.math.pow import kotlin.math.pow
/** /**

View File

@ -1,6 +1,7 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.Manifest import android.Manifest
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
@ -53,23 +54,23 @@ fun YoutubeVideo.getData(): Format?{
} }
} }
} }
fun openPlatform(packageName:String, websiteAddress:String){ fun openPlatform(packageName:String, websiteAddress:String,context: Context){
val manager: PackageManager = mainActivity.packageManager val manager: PackageManager = context.packageManager
try { try {
val intent = manager.getLaunchIntentForPackage(packageName) val intent = manager.getLaunchIntentForPackage(packageName)
?: throw PackageManager.NameNotFoundException() ?: throw PackageManager.NameNotFoundException()
intent.addCategory(Intent.CATEGORY_LAUNCHER) intent.addCategory(Intent.CATEGORY_LAUNCHER)
mainActivity.startActivity(intent) context.startActivity(intent)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
val uri: Uri = val uri: Uri =
Uri.parse(websiteAddress) Uri.parse(websiteAddress)
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
mainActivity.startActivity(intent) context.startActivity(intent)
} }
} }
fun openPlatform(websiteAddress:String){ fun openPlatform(websiteAddress:String,context: Context){
val uri = Uri.parse(websiteAddress) val uri = Uri.parse(websiteAddress)
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
mainActivity.startActivity(intent) context.startActivity(intent)
} }

View File

@ -17,18 +17,23 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
import javax.inject.Inject
const val NoInternetErrorCode = 222 const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor { class NetworkInterceptor @Inject constructor(
@ApplicationContext private val ctx : Context
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
log("Network Requesting Debug",chain.request().url.toString()) log("Network Requesting Debug",chain.request().url.toString())
return if (!isOnline()){ return if (!isOnline(ctx)){
//No Internet Connection //No Internet Connection
showDialog() showDialog()
//Lets Stop the Incoming Request and send Dummy Response //Lets Stop the Incoming Request and send Dummy Response

View File

@ -1,8 +1,6 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.content.Context 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.edit
import androidx.datastore.preferences.core.preferencesKey import androidx.datastore.preferences.core.preferencesKey
import androidx.datastore.preferences.createDataStore import androidx.datastore.preferences.createDataStore
@ -12,7 +10,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class TokenStore ( class TokenStore (
context: Context, context: Context,

View File

@ -17,58 +17,50 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
/**
* mainActivity Instance to use whereEver Needed , as Its God Activity.
* (i.e, almost Active Throughout App's Lifecycle )
*/
val mainActivity
get() = MainActivity.getInstance()
val sharedViewModel val sharedViewModel
get() = MainActivity.getSharedViewModel() get() = MainActivity.getSharedViewModel()
fun loadAllImages( images:List<String>? = null,source: Source, context: Context? = mainActivity) { fun loadAllImages( images:List<String>? = null,source: Source, context: Context) {
val serviceIntent = Intent(context, ForegroundService::class.java) val serviceIntent = Intent(context, ForegroundService::class.java)
images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) } images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun downloadTracks( fun downloadTracks(
trackList: ArrayList<TrackDetails>, trackList: ArrayList<TrackDetails>,
context: Context? = mainActivity context: Context
) { ) {
if(!trackList.isNullOrEmpty()){ if(!trackList.isNullOrEmpty()){
loadAllImages( loadAllImages(
trackList.map { it.albumArtURL }, trackList.map { it.albumArtURL },
trackList.first().source trackList.first().source,
context
) )
val serviceIntent = Intent(context, ForegroundService::class.java) val serviceIntent = Intent(context, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("object",trackList) serviceIntent.putParcelableArrayListExtra("object",trackList)
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
} }
fun queryActiveTracks(context:Context? = mainActivity) { fun queryActiveTracks(context:Context?) {
val serviceIntent = Intent(context, ForegroundService::class.java).apply { val serviceIntent = Intent(context, ForegroundService::class.java).apply {
action = "query" action = "query"
} }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
fun finalOutputDir(itemName:String ,type:String, subFolder:String,extension:String = ".mp3"): String{ defaultDir + removeIllegalChars(type) + File.separator +
return Provider.defaultDir + removeIllegalChars(type) + File.separator + if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + File.separator} +
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(itemName) + extension
removeIllegalChars(itemName) + extension
}
/** /**
* Util. Function To Check Connection Status * Util. Function To Check Connection Status
* */ * */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun isOnline(): Boolean { fun isOnline(ctx:Context): Boolean {
var result = false var result = false
val connectivityManager = val connectivityManager =
mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let { connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply { it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
@ -81,7 +73,7 @@ fun isOnline(): Boolean {
} }
} else { } else {
val netInfo = val netInfo =
(mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo (ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
result = netInfo != null && netInfo.isConnected result = netInfo != null && netInfo.isConnected
} }
} }
@ -92,7 +84,7 @@ fun isOnline(): Boolean {
fun showDialog(title:String? = null, message: String? = null,response: String = "Ok"){ fun showDialog(title:String? = null, message: String? = null,response: String = "Ok"){
//TODO //TODO
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
Toast.makeText(mainActivity,title ?: "No Internet",Toast.LENGTH_SHORT).show() Toast.makeText(MainActivity.getInstance(),title ?: "No Internet",Toast.LENGTH_SHORT).show()
} }
} }
@ -161,11 +153,11 @@ fun removeIllegalChars(fileName: String): String {
return name return name
} }
fun createDirectories() { fun createDirectories(defaultDir:String,imageDir:String) {
createDirectory(Provider.defaultDir) createDirectory(defaultDir)
createDirectory(Provider.imageDir()) createDirectory(imageDir)
createDirectory(Provider.defaultDir + "Tracks/") createDirectory(defaultDir + "Tracks/")
createDirectory(Provider.defaultDir + "Albums/") createDirectory(defaultDir + "Albums/")
createDirectory(Provider.defaultDir + "Playlists/") createDirectory(defaultDir + "Playlists/")
createDirectory(Provider.defaultDir + "YT_Downloads/") createDirectory(defaultDir + "YT_Downloads/")
} }

View File

@ -41,18 +41,18 @@ import com.arthenica.mobileffmpeg.FFmpeg
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.downloadHelper.getYTTracks import com.shabinder.spotiflyer.di.Directories
import com.shabinder.spotiflyer.downloadHelper.sortByBestMatch import com.shabinder.spotiflyer.providers.getYTTracks
import com.shabinder.spotiflyer.providers.sortByBestMatch
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.YoutubeMusicApi import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.tonyodev.fetch2.* import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.EntryPoints
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.* import kotlinx.coroutines.*
import retrofit2.Call import retrofit2.Call
@ -81,13 +81,16 @@ class ForegroundService : Service(){
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
private var messageList = mutableListOf("", "", "", "","") private var messageList = mutableListOf("", "", "", "","")
private val imageDir:String
get() = imageDir(this)
private lateinit var cancelIntent:PendingIntent private lateinit var cancelIntent:PendingIntent
private lateinit var fetch:Fetch private lateinit var fetch:Fetch
private lateinit var downloadManager : DownloadManager private lateinit var downloadManager : DownloadManager
@Inject lateinit var ytDownloader: YoutubeDownloader @Inject lateinit var ytDownloader: YoutubeDownloader
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi @Inject lateinit var youtubeMusicApi: YoutubeMusicApi
@Inject lateinit var directories:Directories
private val defaultDir
get() = directories.defaultDir()
private val imageDir
get() = directories.imageDir()
override fun onBind(intent: Intent): IBinder? = null override fun onBind(intent: Intent): IBinder? = null

View File

@ -5,6 +5,8 @@
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="android:textColor">@color/white</item> <item name="android:textColor">@color/white</item>
<item name="android:background">@android:color/black</item>
<item name="android:backgroundTint">@android:color/black</item>
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
</style> </style>