mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Merge pull request #6 from Shabinder/develop
Develop Branch Merge For v1.6
This commit is contained in:
commit
1f11f2bf9b
@ -1,14 +1,22 @@
|
|||||||
<component name="ProjectDictionaryState">
|
<component name="ProjectDictionaryState">
|
||||||
<dictionary name="shabinder">
|
<dictionary name="shabinder">
|
||||||
<words>
|
<words>
|
||||||
|
<w>albumseokey</w>
|
||||||
|
<w>amita</w>
|
||||||
|
<w>cardview</w>
|
||||||
|
<w>cherrypick</w>
|
||||||
<w>downloadrecord</w>
|
<w>downloadrecord</w>
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>ffmpeg</w>
|
<w>ffmpeg</w>
|
||||||
<w>flyer</w>
|
<w>flyer</w>
|
||||||
|
<w>gaana</w>
|
||||||
|
<w>gener</w>
|
||||||
|
<w>hqdefault</w>
|
||||||
<w>insta</w>
|
<w>insta</w>
|
||||||
<w>instagram</w>
|
<w>instagram</w>
|
||||||
<w>jetbrains</w>
|
<w>jetbrains</w>
|
||||||
<w>kotlinx</w>
|
<w>kotlinx</w>
|
||||||
|
<w>linkedin</w>
|
||||||
<w>mainfragment</w>
|
<w>mainfragment</w>
|
||||||
<w>maxresdefault</w>
|
<w>maxresdefault</w>
|
||||||
<w>moshi</w>
|
<w>moshi</w>
|
||||||
@ -17,14 +25,17 @@
|
|||||||
<w>musicplaceholder</w>
|
<w>musicplaceholder</w>
|
||||||
<w>raleway</w>
|
<w>raleway</w>
|
||||||
<w>semibold</w>
|
<w>semibold</w>
|
||||||
|
<w>seokey</w>
|
||||||
<w>shabinder</w>
|
<w>shabinder</w>
|
||||||
<w>singh</w>
|
<w>singh</w>
|
||||||
|
<w>snackbar</w>
|
||||||
<w>spoti</w>
|
<w>spoti</w>
|
||||||
<w>spotiflyer</w>
|
<w>spotiflyer</w>
|
||||||
<w>spotify</w>
|
<w>spotify</w>
|
||||||
<w>spotifydownloader</w>
|
<w>spotifydownloader</w>
|
||||||
<w>spotifyler</w>
|
<w>spotifyler</w>
|
||||||
<w>thru</w>
|
<w>thru</w>
|
||||||
|
<w>weyfdnx</w>
|
||||||
<w>youtu</w>
|
<w>youtu</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
|
@ -21,22 +21,23 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||||
apply plugin: 'dagger.hilt.android.plugin'
|
apply plugin: 'dagger.hilt.android.plugin'
|
||||||
//apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
buildToolsVersion "30.0.2"
|
buildToolsVersion "30.0.2"
|
||||||
|
|
||||||
buildFeatures{
|
buildFeatures{
|
||||||
dataBinding = true
|
//dataBinding = true
|
||||||
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'com.shabinder.spotiflyer'
|
applicationId 'com.shabinder.spotiflyer'
|
||||||
minSdkVersion 22
|
minSdkVersion 22
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 7
|
versionCode 8
|
||||||
versionName "1.5.1"
|
versionName "1.6"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
@ -90,8 +91,10 @@ dependencies {
|
|||||||
implementation 'com.google.android.material:material:1.2.1'
|
implementation 'com.google.android.material:material:1.2.1'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||||
|
|
||||||
implementation "androidx.room:room-runtime:2.2.5"
|
implementation "androidx.room:room-runtime:2.2.5"
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
|
||||||
kapt "androidx.room:room-compiler:2.2.5"
|
kapt "androidx.room:room-compiler:2.2.5"
|
||||||
implementation "androidx.room:room-ktx:2.2.5"
|
implementation "androidx.room:room-ktx:2.2.5"
|
||||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||||
@ -115,10 +118,13 @@ dependencies {
|
|||||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||||
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
||||||
|
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
|
||||||
|
implementation 'com.beust:klaxon:5.4'
|
||||||
|
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||||
|
|
||||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
|
||||||
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.2'
|
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'
|
||||||
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
||||||
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
||||||
|
|
||||||
|
18
app/proguard-rules.pro
vendored
18
app/proguard-rules.pro
vendored
@ -11,7 +11,25 @@
|
|||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
-keepattributes *Annotation*, InnerClasses
|
||||||
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
|
|
||||||
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.* {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.* {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Change here com.yourcompany.yourpackage
|
||||||
|
-keep,includedescriptorclasses class com.shabinder.spotiflyer.**$$serializer { *; } # <-- change package name to your app's
|
||||||
|
-keepclassmembers class com.shabinder.spotiflyer* { # <-- change package name to your app's
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class com.shabinder.spotiflyer.* { # <-- change package name to your app's
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
@ -21,24 +21,24 @@ import android.Manifest
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.findNavController
|
||||||
import com.github.javiersantos.appupdater.AppUpdater
|
import com.github.javiersantos.appupdater.AppUpdater
|
||||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||||
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import com.shabinder.spotiflyer.utils.createDirectories
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -49,49 +49,48 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is App's God Activity
|
||||||
|
* */
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity(){
|
class MainActivity : AppCompatActivity(){
|
||||||
private var spotifyService : SpotifyService? = null
|
private var spotifyService : SpotifyService? = null
|
||||||
private var isConnected: Boolean = false
|
|
||||||
private var sharedPref :SharedPreferences? = null
|
|
||||||
private var token :String =""
|
|
||||||
private lateinit var binding: MainActivityBinding
|
private lateinit var binding: MainActivityBinding
|
||||||
|
lateinit var snackBarAnchor: View
|
||||||
private lateinit var sharedViewModel: SharedViewModel
|
private lateinit var sharedViewModel: SharedViewModel
|
||||||
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
|
private lateinit var navController: NavController
|
||||||
@Inject lateinit var moshi: Moshi
|
@Inject lateinit var moshi: Moshi
|
||||||
|
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
|
||||||
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
|
||||||
//Enabling Dark Mode
|
//Enabling Dark Mode
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||||
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
binding = MainActivityBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
||||||
|
navController = findNavController(R.id.navHostFragment)
|
||||||
|
snackBarAnchor = binding.snackBarPosition
|
||||||
|
|
||||||
//starting Notification and Downloader Service!
|
authenticateSpotify()
|
||||||
SpotifyDownloadHelper.startService(this)
|
|
||||||
|
|
||||||
if(sharedViewModel.spotifyService.value == null){
|
|
||||||
authenticateSpotify()
|
|
||||||
}else{
|
|
||||||
implementSpotifyService(sharedViewModel.accessToken.value!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPermission()
|
requestPermission()
|
||||||
disableDozeMode()
|
disableDozeMode()
|
||||||
checkIfLatestVersion()
|
checkIfLatestVersion()
|
||||||
createDirectories()
|
createDirectories()
|
||||||
isConnected = sharedViewModel.isOnline(this)
|
Log.i("Connection Status", isOnline().toString())
|
||||||
sharedViewModel.isConnected.value = isConnected
|
|
||||||
Log.i("Connection Status", isConnected.toString())
|
//starting Notification and Downloader Service!
|
||||||
|
startService(this)
|
||||||
|
|
||||||
handleIntentFromExternalActivity()
|
handleIntentFromExternalActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
Log.i("NEW INTENT", "Received")
|
//Return to MainFragment For Further Processing of this Intent
|
||||||
|
navController.popBackStack(R.id.mainFragment,false)
|
||||||
handleIntentFromExternalActivity(intent)
|
handleIntentFromExternalActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,9 +101,10 @@ class MainActivity : AppCompatActivity(){
|
|||||||
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
||||||
if (!isIgnoringBatteryOptimizations) {
|
if (!isIgnoringBatteryOptimizations) {
|
||||||
val intent = Intent()
|
val intent = Intent().apply{
|
||||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||||
intent.data = Uri.parse("package:$packageName")
|
data = Uri.parse("package:$packageName")
|
||||||
|
}
|
||||||
startActivityForResult(intent, 1233)
|
startActivityForResult(intent, 1233)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,13 +139,13 @@ class MainActivity : AppCompatActivity(){
|
|||||||
"Bearer $token"
|
"Bearer $token"
|
||||||
).build()
|
).build()
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
})
|
}).addInterceptor(NetworkInterceptor())
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder()
|
val retrofit = Retrofit.Builder()
|
||||||
.baseUrl("https://api.spotify.com/v1/")
|
.baseUrl("https://api.spotify.com/v1/")
|
||||||
.client(httpClient.build())
|
.client(httpClient.build())
|
||||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
spotifyService = retrofit.create(SpotifyService::class.java)
|
spotifyService = retrofit.create(SpotifyService::class.java)
|
||||||
sharedViewModel.spotifyService.value = spotifyService
|
sharedViewModel.spotifyService.value = spotifyService
|
||||||
@ -154,16 +154,13 @@ class MainActivity : AppCompatActivity(){
|
|||||||
|
|
||||||
fun authenticateSpotify() {
|
fun authenticateSpotify() {
|
||||||
sharedViewModel.uiScope.launch {
|
sharedViewModel.uiScope.launch {
|
||||||
if (isConnected) {
|
Log.i("Spotify Authentication","Started")
|
||||||
Log.i("Post Request", "Made")
|
val token = spotifyServiceTokenRequest.getToken()
|
||||||
token = spotifyServiceTokenRequest.getToken()!!.access_token
|
token.value?.let {
|
||||||
implementSpotifyService(token)
|
showMessage("Success: Spotify Token Acquired",isSuccess = true)
|
||||||
Log.i("Post Request", token)
|
implementSpotifyService(it.access_token)
|
||||||
sharedViewModel.accessToken.value = token
|
|
||||||
}else{
|
|
||||||
Log.i("network", "unavailable")
|
|
||||||
// sharedViewModel.showAlertDialog(resources,this@MainActivity)
|
|
||||||
}
|
}
|
||||||
|
Log.i("Spotify Token", token.value.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,25 +185,14 @@ class MainActivity : AppCompatActivity(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
|
||||||
savedInstanceState.putString("token", token)
|
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
|
||||||
}
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
|
||||||
if (savedInstanceState.getString("token") ==""){
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
}else{
|
|
||||||
implementSpotifyService(savedInstanceState.getString("token")!!)
|
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkIfLatestVersion() {
|
private fun checkIfLatestVersion() {
|
||||||
val appUpdater = AppUpdater(this)
|
val appUpdater = AppUpdater(this)
|
||||||
.showAppUpdated(false)//true:Show App is Update Dialog
|
.showAppUpdated(false)//true:Show App is Update Dialog
|
||||||
.setUpdateFrom(UpdateFrom.XML)
|
.setUpdateFrom(UpdateFrom.XML)
|
||||||
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
|
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
|
.setButtonDoNotShowAgain("Remind Later")
|
||||||
|
.setButtonDoNotShowAgainClickListener { dialog, _ -> dialog.dismiss() }
|
||||||
.setButtonUpdateClickListener { _, _ ->
|
.setButtonUpdateClickListener { _, _ ->
|
||||||
val uri: Uri =
|
val uri: Uri =
|
||||||
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
|
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
|
||||||
@ -220,11 +206,10 @@ class MainActivity : AppCompatActivity(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object{
|
companion object{
|
||||||
private var instance = MainActivity()
|
private lateinit var instance: MainActivity
|
||||||
fun getInstance():MainActivity{
|
fun getInstance():MainActivity = instance
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
}
|
}
|
||||||
|
@ -17,49 +17,22 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer
|
package com.shabinder.spotiflyer
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class SharedViewModel : ViewModel() {
|
class SharedViewModel : ViewModel() {
|
||||||
var intentString = MutableLiveData<String>().apply { value = "" }
|
var intentString = MutableLiveData<String>()
|
||||||
var spotifyService = MutableLiveData<SpotifyService>()
|
var spotifyService = MutableLiveData<SpotifyService>()
|
||||||
var accessToken = MutableLiveData<String>().apply { value = "" }
|
|
||||||
var isConnected = MutableLiveData<Boolean>().apply { value = false }
|
|
||||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator
|
|
||||||
|
|
||||||
|
|
||||||
private var viewModelJob = Job()
|
private var viewModelJob = Job()
|
||||||
|
|
||||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
viewModelJob.cancel()
|
viewModelJob.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showAlertDialog(resources:Resources,context: Context){
|
|
||||||
MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
|
|
||||||
.setTitle(resources.getString(R.string.title))
|
|
||||||
.setMessage(resources.getString(R.string.supporting_text))
|
|
||||||
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
|
|
||||||
// Respond to neutral button press
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
fun isOnline(context: Context): Boolean {
|
|
||||||
val cm =
|
|
||||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val netInfo = cm.activeNetworkInfo
|
|
||||||
return netInfo != null && netInfo.isConnectedOrConnecting
|
|
||||||
}
|
|
||||||
}
|
}
|
166
app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt
Executable file
166
app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt
Executable file
@ -0,0 +1,166 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.Handler
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||||
|
import com.shabinder.spotiflyer.networking.makeJsonBody
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object DownloadHelper {
|
||||||
|
|
||||||
|
var statusBar:TextView? = null
|
||||||
|
var youtubeMusicApi: YoutubeMusicApi? = null
|
||||||
|
var sharedViewModel: SharedViewModel? = null
|
||||||
|
|
||||||
|
private var total = 0
|
||||||
|
private var processed = 0
|
||||||
|
var notFound = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function To Download All Tracks Available in a List
|
||||||
|
**/
|
||||||
|
suspend fun downloadAllTracks(
|
||||||
|
type:String,
|
||||||
|
subFolder: String?,
|
||||||
|
trackList: List<TrackDetails>) {
|
||||||
|
resetStatusBar()// For New Download Request's Status
|
||||||
|
val downloadList = ArrayList<DownloadObject>()
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
total += trackList.size // Adding New Download List Count to StatusBar
|
||||||
|
trackList.forEachIndexed { index, it ->
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
|
||||||
|
processed++
|
||||||
|
if(index == (trackList.size-1)){//LastElement
|
||||||
|
Handler().postDelayed({
|
||||||
|
//Delay is Added ,if a request is in processing it may finish
|
||||||
|
Log.i("Spotify Helper","Download Request Sent")
|
||||||
|
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||||
|
showMessage("Download Started, Now You can leave the App!")
|
||||||
|
}
|
||||||
|
startService(mainActivity,downloadList)
|
||||||
|
},3000)
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
|
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
|
||||||
|
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
|
||||||
|
object : Callback<String>{
|
||||||
|
override fun onResponse(call: Call<String>, response: Response<String>) {
|
||||||
|
sharedViewModel?.uiScope?.launch {
|
||||||
|
val videoId = sortByBestMatch(
|
||||||
|
getYTTracks(response.body().toString()),
|
||||||
|
trackName = it.title,
|
||||||
|
trackArtists = it.artists,
|
||||||
|
trackDurationSec = it.durationSec
|
||||||
|
).keys.firstOrNull()
|
||||||
|
Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
|
||||||
|
|
||||||
|
if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()}
|
||||||
|
else {//Found Youtube Video ID
|
||||||
|
val outputFile: String =
|
||||||
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
defaultDir +
|
||||||
|
removeIllegalChars(type) + File.separator +
|
||||||
|
(if (subFolder == null) { "" }
|
||||||
|
else { removeIllegalChars(subFolder) + File.separator }
|
||||||
|
+ removeIllegalChars(it.title) + ".m4a")
|
||||||
|
|
||||||
|
val downloadObject = DownloadObject(
|
||||||
|
trackDetails = it,
|
||||||
|
ytVideoId = videoId,
|
||||||
|
outputFile = outputFile
|
||||||
|
)
|
||||||
|
processed++
|
||||||
|
sharedViewModel?.uiScope?.launch(Dispatchers.Main) {
|
||||||
|
updateStatusBar()
|
||||||
|
}
|
||||||
|
downloadList.add(downloadObject)
|
||||||
|
if(index == (trackList.size-1)){//LastElement
|
||||||
|
Handler().postDelayed({
|
||||||
|
//Delay is Added ,if a request is in processing it may finish
|
||||||
|
Log.i("Spotify Helper","Download Request Sent")
|
||||||
|
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
|
||||||
|
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
startService(mainActivity,downloadList)
|
||||||
|
},5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onFailure(call: Call<String>, t: Throwable) {
|
||||||
|
if(t.message.toString().contains("Failed to connect")) showMessage("Failed, Check Your Internet Connection!")
|
||||||
|
Log.i("YT API Req. Fail",t.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateStatusBar()
|
||||||
|
}
|
||||||
|
animateStatusBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetStatusBar() {
|
||||||
|
total = 0
|
||||||
|
processed = 0
|
||||||
|
notFound = 0
|
||||||
|
updateStatusBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateStatusBar() {
|
||||||
|
val anim: Animation = AlphaAnimation(0.3f, 0.9f)
|
||||||
|
anim.duration = 1500 //You can manage the blinking time with this parameter
|
||||||
|
anim.startOffset = 20
|
||||||
|
anim.repeatMode = Animation.REVERSE
|
||||||
|
anim.repeatCount = Animation.INFINITE
|
||||||
|
statusBar?.animation = anim
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
fun updateStatusBar() {
|
||||||
|
statusBar!!.visibility = View.VISIBLE
|
||||||
|
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
||||||
|
}
|
||||||
|
}
|
@ -1,289 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.os.Handler
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.AlphaAnimation
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.webkit.WebView
|
|
||||||
import android.webkit.WebViewClient
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
|
||||||
import com.shabinder.spotiflyer.models.Track
|
|
||||||
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
|
||||||
import com.shabinder.spotiflyer.utils.getEmojiByUnicode
|
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object SpotifyDownloadHelper {
|
|
||||||
var webView:WebView? = null
|
|
||||||
var context : Context? = null
|
|
||||||
var statusBar:TextView? = null
|
|
||||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
|
||||||
var spotifyViewModel: SpotifyViewModel? = null
|
|
||||||
private var isBrowserLoading = false
|
|
||||||
private var total = 0
|
|
||||||
private var Processed = 0
|
|
||||||
private var notFound = 0
|
|
||||||
private var listProcessed:Boolean = false
|
|
||||||
var youtubeList = mutableListOf<YoutubeRequest>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function To Download All Tracks Available in a List
|
|
||||||
**/
|
|
||||||
suspend fun downloadAllTracks(
|
|
||||||
type:String,
|
|
||||||
subFolder: String?,
|
|
||||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
|
||||||
withContext(Dispatchers.Main){
|
|
||||||
total += trackList.size // Adding New Download List Count to StatusBar
|
|
||||||
trackList.forEach {
|
|
||||||
if(it.downloaded == "Downloaded"){//Download Already Present!!
|
|
||||||
Processed++
|
|
||||||
}else{
|
|
||||||
if(isBrowserLoading){//WebView Busy!!
|
|
||||||
if (listProcessed){//Previous List request progress check
|
|
||||||
getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
|
||||||
listProcessed = false//Notifying A list Processing Started
|
|
||||||
}else{//Adding Requests to a Queue
|
|
||||||
youtubeList.add(YoutubeRequest(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it))
|
|
||||||
}
|
|
||||||
}else{
|
|
||||||
getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateStatusBar()
|
|
||||||
}
|
|
||||||
animateStatusBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//TODO CleanUp here and there!!
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
suspend fun getYTLink(type:String,
|
|
||||||
subFolder:String?,
|
|
||||||
ytDownloader: YoutubeDownloader?,
|
|
||||||
searchQuery: String,
|
|
||||||
track: Track){
|
|
||||||
isBrowserLoading = true // Notify Web View Started Loading
|
|
||||||
val searchText = searchQuery.replace("\\s".toRegex(), "+")
|
|
||||||
val url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=$searchText"
|
|
||||||
Log.i("DH YT LINK ",url)
|
|
||||||
applyWebViewSettings(webView!!)
|
|
||||||
withContext(Dispatchers.Main){
|
|
||||||
webView!!.loadUrl(url)
|
|
||||||
webView!!.webViewClient = object : WebViewClient() {
|
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
|
||||||
super.onPageFinished(view, url)
|
|
||||||
view?.evaluateJavascript(
|
|
||||||
"document.getElementsByClassName(\"yt-simple-endpoint style-scope ytd-video-renderer\")[0].href"
|
|
||||||
) { value ->
|
|
||||||
Log.i("YT-id Link", value.toString().replace("\"", ""))
|
|
||||||
val id = value!!.substringAfterLast("=", "error").replace("\"", "")
|
|
||||||
Log.i("YT-ID", id)
|
|
||||||
if (id != "error") {//Link extracting error
|
|
||||||
Processed++
|
|
||||||
downloadFile(subFolder, type, track, ytDownloader, id)
|
|
||||||
}else notFound++
|
|
||||||
updateStatusBar()
|
|
||||||
if (youtubeList.isNotEmpty()) {
|
|
||||||
val request = youtubeList[0]
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
getYTLink(
|
|
||||||
request.type,
|
|
||||||
request.subFolder,
|
|
||||||
request.ytDownloader,
|
|
||||||
request.searchQuery,
|
|
||||||
request.track
|
|
||||||
)
|
|
||||||
}
|
|
||||||
youtubeList.remove(request)
|
|
||||||
if (youtubeList.size == 0) {//list processing completed , webView is free again!
|
|
||||||
isBrowserLoading = false
|
|
||||||
listProcessed = true
|
|
||||||
}
|
|
||||||
} else {//YT List Empty....Maybe it was one Single Download
|
|
||||||
Handler().postDelayed({//Delay of 1.5 sec
|
|
||||||
if (youtubeList.isEmpty()) {//Lets Make It sure , There are No more Downloads In Queue.....
|
|
||||||
isBrowserLoading = false
|
|
||||||
listProcessed = true
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateStatusBar() {
|
|
||||||
statusBar!!.visibility = View.VISIBLE
|
|
||||||
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $Processed ${getEmojiByUnicode(0x274C)}: $notFound"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun downloadFile(subFolder: String?, type: String, track:Track, ytDownloader: YoutubeDownloader?, id: String) {
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val video = ytDownloader?.getVideo(id)
|
|
||||||
val detail = video?.details()
|
|
||||||
val format: Format? = try {
|
|
||||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
try {
|
|
||||||
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
|
||||||
Log.i("YTDownloader", e.toString())
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format?.let {
|
|
||||||
val url: String = format.url()
|
|
||||||
Log.i("DHelper Link Found", url)
|
|
||||||
val outputFile: String =
|
|
||||||
Environment.getExternalStorageDirectory().toString() + File.separator +
|
|
||||||
defaultDir + removeIllegalChars(type) + File.separator + (if (subFolder == null) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
removeIllegalChars(subFolder) + File.separator
|
|
||||||
} + removeIllegalChars(track.name!!) + ".m4a")
|
|
||||||
|
|
||||||
val downloadObject = DownloadObject(
|
|
||||||
track = track,
|
|
||||||
url = url,
|
|
||||||
outputDir = outputFile
|
|
||||||
)
|
|
||||||
Log.i("DH", outputFile)
|
|
||||||
startService(context!!, downloadObject)
|
|
||||||
}
|
|
||||||
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
|
||||||
Log.i("DH", "Error- Maybe Network")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun startService(context:Context,obj:DownloadObject? = null ) {
|
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
||||||
serviceIntent.putExtra("object",obj)
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removing Illegal Chars from File Name
|
|
||||||
* **/
|
|
||||||
fun removeIllegalChars(fileName: String): String? {
|
|
||||||
val illegalCharArray = charArrayOf(
|
|
||||||
'/',
|
|
||||||
'\n',
|
|
||||||
'\r',
|
|
||||||
'\t',
|
|
||||||
'\u0000',
|
|
||||||
'\u000C',
|
|
||||||
'`',
|
|
||||||
'?',
|
|
||||||
'*',
|
|
||||||
'\\',
|
|
||||||
'<',
|
|
||||||
'>',
|
|
||||||
'|',
|
|
||||||
'\"',
|
|
||||||
'.',
|
|
||||||
'-',
|
|
||||||
'\''
|
|
||||||
)
|
|
||||||
|
|
||||||
var name = fileName
|
|
||||||
for (c in illegalCharArray) {
|
|
||||||
name = fileName.replace(c, '_')
|
|
||||||
}
|
|
||||||
name = name.replace("\\s".toRegex(), "_")
|
|
||||||
name = name.replace("\\)".toRegex(), "")
|
|
||||||
name = name.replace("\\(".toRegex(), "")
|
|
||||||
name = name.replace("\\[".toRegex(), "")
|
|
||||||
name = name.replace("]".toRegex(), "")
|
|
||||||
name = name.replace("\\.".toRegex(), "")
|
|
||||||
name = name.replace("\"".toRegex(), "")
|
|
||||||
name = name.replace("\'".toRegex(), "")
|
|
||||||
name = name.replace(":".toRegex(), "")
|
|
||||||
name = name.replace("\\|".toRegex(), "")
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateStatusBar() {
|
|
||||||
val anim: Animation = AlphaAnimation(0.0f, 0.9f)
|
|
||||||
anim.duration = 650 //You can manage the blinking time with this parameter
|
|
||||||
anim.startOffset = 20
|
|
||||||
anim.repeatMode = Animation.REVERSE
|
|
||||||
anim.repeatCount = Animation.INFINITE
|
|
||||||
statusBar?.animation = anim
|
|
||||||
}
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
|
||||||
fun applyWebViewSettings(webView: WebView) {
|
|
||||||
val desktopUserAgent =
|
|
||||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0"
|
|
||||||
val mobileUserAgent =
|
|
||||||
"Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"
|
|
||||||
|
|
||||||
//Choose Mobile/Desktop client.
|
|
||||||
webView.settings.userAgentString = desktopUserAgent
|
|
||||||
webView.settings.loadWithOverviewMode = true
|
|
||||||
webView.settings.builtInZoomControls = true
|
|
||||||
webView.settings.setSupportZoom(true)
|
|
||||||
webView.isScrollbarFadingEnabled = false
|
|
||||||
webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY
|
|
||||||
webView.settings.displayZoomControls = false
|
|
||||||
webView.settings.useWideViewPort = true
|
|
||||||
webView.settings.javaScriptEnabled = true
|
|
||||||
webView.settings.loadsImagesAutomatically = false
|
|
||||||
webView.settings.blockNetworkImage = true
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
webView.settings.safeBrowsingEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data class YoutubeRequest(
|
|
||||||
val type:String,
|
|
||||||
val subFolder:String?,
|
|
||||||
val ytDownloader: YoutubeDownloader?,
|
|
||||||
val searchQuery: String,
|
|
||||||
val track: Track,
|
|
||||||
val index: Int? = null
|
|
||||||
)
|
|
@ -17,49 +17,54 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.downloadHelper
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.widget.Toast
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
|
import com.shabinder.spotiflyer.utils.isOnline
|
||||||
|
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||||
|
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
|
||||||
|
import com.shabinder.spotiflyer.utils.startService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object YTDownloadHelper {
|
object YTDownloadHelper {
|
||||||
var context : Context? = null
|
suspend fun downloadYTTracks(
|
||||||
var statusBar: TextView? = null
|
type:String,
|
||||||
|
subFolder: String?,
|
||||||
fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) {
|
tracks:List<TrackDetails>,
|
||||||
format?.let {
|
){
|
||||||
val url:String = format.url()
|
val downloadList = ArrayList<DownloadObject>()
|
||||||
// Log.i("DHelper Link Found", url)
|
tracks.forEach {
|
||||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
if(!isOnline()){
|
||||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars(
|
showNoConnectionAlert()
|
||||||
ytTrack.name!!
|
return
|
||||||
) +".m4a")
|
}
|
||||||
|
val outputFile: String =
|
||||||
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
defaultDir +
|
||||||
|
removeIllegalChars(type) + File.separator +
|
||||||
|
(if (subFolder == null) { "" }
|
||||||
|
else { removeIllegalChars(subFolder) + File.separator }
|
||||||
|
+ removeIllegalChars(it.title) + ".m4a")
|
||||||
|
|
||||||
val downloadObject = DownloadObject(
|
val downloadObject = DownloadObject(
|
||||||
track = ytTrack,
|
trackDetails = it,
|
||||||
url = url,
|
ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
|
||||||
outputDir = outputFile
|
.substringBeforeLast("."),
|
||||||
|
outputFile = outputFile
|
||||||
)
|
)
|
||||||
Log.i("DH",outputFile)
|
|
||||||
startService(context!!, downloadObject)
|
downloadList.add(downloadObject)
|
||||||
statusBar?.visibility= View.VISIBLE
|
}
|
||||||
|
Log.i("YT Downloader Helper","Download Request Sent")
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
|
||||||
|
startService(mainActivity,downloadList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun startService(context:Context, obj: DownloadObject? = null ) {
|
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
|
||||||
serviceIntent.putExtra("object",obj)
|
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.downloadHelper
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import com.beust.klaxon.JsonArray
|
||||||
|
import com.beust.klaxon.JsonObject
|
||||||
|
import com.beust.klaxon.Parser
|
||||||
|
import com.shabinder.spotiflyer.models.YoutubeTrack
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Thanks To https://github.com/spotDL/spotify-downloader
|
||||||
|
* */
|
||||||
|
fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||||
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
|
|
||||||
|
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||||
|
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
||||||
|
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
||||||
|
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||||
|
if (contentBlocks != null) {
|
||||||
|
for (cBlock in contentBlocks){
|
||||||
|
/**
|
||||||
|
*Ignore user-suggestion
|
||||||
|
*The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
|
||||||
|
*results for xyz, search for abc instead') we have no use for them, the for
|
||||||
|
*loop below if throw a keyError if we don't ignore them
|
||||||
|
*/
|
||||||
|
if(cBlock.containsKey("itemSectionRenderer")){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
||||||
|
/**
|
||||||
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
|
* I have no clue what they are and why there even exist
|
||||||
|
*
|
||||||
|
if(!contents.containsKey("overlay")){
|
||||||
|
println(contents)
|
||||||
|
continue
|
||||||
|
TODO check and correct
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val result = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.array<JsonObject>("flexColumns")
|
||||||
|
|
||||||
|
//Add the linkBlock
|
||||||
|
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.obj("overlay")
|
||||||
|
?.obj("musicItemThumbnailOverlayRenderer")
|
||||||
|
?.obj("content")
|
||||||
|
?.obj("musicPlayButtonRenderer")
|
||||||
|
?.obj("playNavigationEndpoint")
|
||||||
|
|
||||||
|
// detailsBlock is always a list, so we just append the linkBlock to it
|
||||||
|
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
||||||
|
linkBlock?.let { result?.add(it) }
|
||||||
|
result?.let { resultBlocks.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
||||||
|
! Songs and Videos are supplied with different details, extracting all details from
|
||||||
|
! both is just carrying on redundant data, so we also have to selectively extract
|
||||||
|
! relevant details. What you need to know to understand how we do that here:
|
||||||
|
!
|
||||||
|
! Songs details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Song)
|
||||||
|
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
||||||
|
! 3 - Album
|
||||||
|
! 4 - Duration (mm:ss)
|
||||||
|
!
|
||||||
|
! Video details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Video)
|
||||||
|
! 2 - Channel
|
||||||
|
! 3 - Viewers
|
||||||
|
! 4 - Duration (hh:mm:ss)
|
||||||
|
!
|
||||||
|
! We blindly gather all the details we get our hands on, then
|
||||||
|
! cherrypick the details we need based on their index numbers,
|
||||||
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
|
*/
|
||||||
|
|
||||||
|
for(result in resultBlocks){
|
||||||
|
|
||||||
|
// Blindly gather available details
|
||||||
|
val availableDetails = mutableListOf<String>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Filter Out dummies here itself
|
||||||
|
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||||
|
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
||||||
|
! I have no clue. We skip these.
|
||||||
|
|
||||||
|
! Remember that we appended the linkBlock to result, treating that like the
|
||||||
|
! other constituents of a result block will lead to errors, hence the 'in
|
||||||
|
! result[:-1] ,i.e., skip last element in array '
|
||||||
|
*/
|
||||||
|
for(detail in result.subList(0,result.size-1)){
|
||||||
|
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||||
|
|
||||||
|
// if not a dummy, collect All Variables
|
||||||
|
detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
?.obj("text")
|
||||||
|
?.array<JsonObject>("runs")?.get(0)?.get("text")?.let {
|
||||||
|
availableDetails.add(
|
||||||
|
it.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.i("Text Api",availableDetails.toString())
|
||||||
|
/*
|
||||||
|
! Filter Out non-Song/Video results and incomplete results here itself
|
||||||
|
! From what we know about detail order, note that [1] - indicate result type
|
||||||
|
*/
|
||||||
|
if ( availableDetails.size == 5 && availableDetails[1] in listOf("Song","Video") ){
|
||||||
|
|
||||||
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
|
if(availableDetails[4].split(':').size != 2) continue //Has Been Giving Issues
|
||||||
|
|
||||||
|
/*
|
||||||
|
! grab Video ID
|
||||||
|
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||||
|
! so hardcoding the dict keys for data look up is an ardours process, since
|
||||||
|
! the sub-block pattern is fixed even though the key isn't, we just
|
||||||
|
! reference the dict keys by index
|
||||||
|
*/
|
||||||
|
|
||||||
|
val videoId:String = result.last().obj("watchEndpoint")?.get("videoId") as String
|
||||||
|
val ytTrack = YoutubeTrack(
|
||||||
|
name = availableDetails[0],
|
||||||
|
type = availableDetails[1],
|
||||||
|
artist = availableDetails[2],
|
||||||
|
duration = availableDetails[4],
|
||||||
|
videoId = videoId
|
||||||
|
)
|
||||||
|
youtubeTracks.add(ytTrack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return youtubeTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
fun sortByBestMatch(ytTracks:List<YoutubeTrack>,
|
||||||
|
trackName:String,
|
||||||
|
trackArtists:List<String>,
|
||||||
|
trackDurationSec:Int,
|
||||||
|
):Map<String,Int>{
|
||||||
|
/*
|
||||||
|
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
|
||||||
|
**/
|
||||||
|
val linksWithMatchValue = mutableMapOf<String,Int>()
|
||||||
|
|
||||||
|
for (result in ytTracks){
|
||||||
|
|
||||||
|
// LoweCasing Name to match Properly
|
||||||
|
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||||
|
var hasCommonWord = false
|
||||||
|
|
||||||
|
val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: ""
|
||||||
|
val trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
|
for (nameWord in trackNameWords){
|
||||||
|
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip this Result if No Word is Common in Name
|
||||||
|
if (!hasCommonWord) {
|
||||||
|
Log.i("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find artist match
|
||||||
|
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
|
||||||
|
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
|
||||||
|
var artistMatchNumber = 0
|
||||||
|
|
||||||
|
if(result.type == "Song"){
|
||||||
|
for (artist in trackArtists){
|
||||||
|
if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}else{//i.e. is a Video
|
||||||
|
for (artist in trackArtists) {
|
||||||
|
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(artistMatchNumber == 0) {
|
||||||
|
Log.i("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
||||||
|
|
||||||
|
// Duration Match
|
||||||
|
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||||
|
! difference in song duration (delta) is usually of the magnitude of a few
|
||||||
|
! seconds, we need to amplify the delta if it is to have any meaningful impact
|
||||||
|
! wen we calculate the avg match value*/
|
||||||
|
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
|
||||||
|
?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0)
|
||||||
|
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||||
|
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
||||||
|
val durationMatch = 100 - (nonMatchValue*100)
|
||||||
|
|
||||||
|
val avgMatch = (artistMatch + durationMatch)/2
|
||||||
|
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||||
|
}
|
||||||
|
Log.i("YT Api Result", "$trackName - $linksWithMatchValue")
|
||||||
|
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||||
|
}
|
@ -18,12 +18,35 @@
|
|||||||
package com.shabinder.spotiflyer.models
|
package com.shabinder.spotiflyer.models
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DownloadObject(
|
data class DownloadObject(
|
||||||
var ytVideo: YTTrack?=null,
|
var trackDetails: TrackDetails,
|
||||||
var track: Track?=null,
|
var ytVideoId:String,
|
||||||
var url:String,
|
var outputFile:String
|
||||||
var outputDir:String
|
):Parcelable
|
||||||
):Parcelable
|
|
||||||
|
@Parcelize
|
||||||
|
data class TrackDetails(
|
||||||
|
var title:String,
|
||||||
|
var artists:List<String>,
|
||||||
|
var durationSec:Int,
|
||||||
|
var albumName:String?=null,
|
||||||
|
var year:String?=null,
|
||||||
|
var comment:String?=null,
|
||||||
|
var lyrics:String?=null,
|
||||||
|
var trackUrl:String?=null,
|
||||||
|
var albumArt: File,
|
||||||
|
var albumArtURL: String,
|
||||||
|
var source: Source,
|
||||||
|
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||||
|
):Parcelable
|
||||||
|
|
||||||
|
enum class DownloadStatus{
|
||||||
|
Downloaded,
|
||||||
|
Downloading,
|
||||||
|
NotDownloaded
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Optional<T>(val value: T?)
|
13
app/src/main/java/com/shabinder/spotiflyer/models/YTTrack.kt → app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt
Executable file → Normal file
13
app/src/main/java/com/shabinder/spotiflyer/models/YTTrack.kt → app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt
Executable file → Normal file
@ -21,11 +21,10 @@ import android.os.Parcelable
|
|||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class YTTrack(
|
data class YoutubeTrack(
|
||||||
var id:String?,
|
var name: String? = null,
|
||||||
var title:String?,
|
var type: String? = null, // Song / Video
|
||||||
var duration:Int?,
|
var artist: String? = null,
|
||||||
var author:String?,
|
var duration:String? = null,
|
||||||
var viewCount:Long?,
|
var videoId: String? = null
|
||||||
var thumbnails:List<String?>?
|
|
||||||
):Parcelable
|
):Parcelable
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
data class Artist (
|
||||||
|
val popularity : Int,
|
||||||
|
val seokey : String,
|
||||||
|
val name : String,
|
||||||
|
@Json(name = "artwork_175x175")var artworkLink :String?
|
||||||
|
)
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
data class CustomArtworks (
|
||||||
|
@Json(name = "40x40") val size_40p : String,
|
||||||
|
@Json(name = "80x80") val size_80p : String,
|
||||||
|
@Json(name = "110x110")val size_110p : String,
|
||||||
|
@Json(name = "175x175")val size_175p : String,
|
||||||
|
@Json(name = "480x480")val size_480p : String,
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class GaanaAlbum (
|
||||||
|
val tracks : List<GaanaTrack>,
|
||||||
|
val count : Int,
|
||||||
|
val custom_artworks : CustomArtworks,
|
||||||
|
val release_year : Int,
|
||||||
|
val favorite_count : Int,
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class GaanaArtistDetails(
|
||||||
|
val artist : List<Artist>,
|
||||||
|
val count : Int,
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class GaanaArtistTracks(
|
||||||
|
val count : Int,
|
||||||
|
val tracks : List<GaanaTrack>
|
||||||
|
)
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class GaanaPlaylist (
|
||||||
|
val tags : String?,
|
||||||
|
val modified_on : String,
|
||||||
|
val count : Int,
|
||||||
|
val created_on : String,
|
||||||
|
val favorite_count : Int,
|
||||||
|
val tracks : List<GaanaTrack>,
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class GaanaSong(
|
||||||
|
val tracks : List<GaanaTrack>
|
||||||
|
)
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
|
||||||
|
data class GaanaTrack (
|
||||||
|
val tags : List<Tags?>?,
|
||||||
|
val seokey : String,
|
||||||
|
val albumseokey : String?,
|
||||||
|
val track_title : String,
|
||||||
|
val album_title : String?,
|
||||||
|
val language : String?,
|
||||||
|
val duration: Int,
|
||||||
|
@Json(name = "artwork_large") val artworkLink : String,
|
||||||
|
val artist : List<Artist?>,
|
||||||
|
@Json(name = "gener") val genre : List<Genre?>?,
|
||||||
|
val lyrics_url : String?,
|
||||||
|
val youtube_id : String?,
|
||||||
|
val total_favourite_count : Int?,
|
||||||
|
val release_date : String?,
|
||||||
|
val play_ct : String?,
|
||||||
|
val secondary_language : String?,
|
||||||
|
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class Genre (
|
||||||
|
val genre_id : Int,
|
||||||
|
val name : String
|
||||||
|
)
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.gaana
|
||||||
|
|
||||||
|
data class Tags (
|
||||||
|
val tag_id : Int,
|
||||||
|
val tag_name : String
|
||||||
|
)
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
|
enum class Source {
|
||||||
|
Spotify,
|
||||||
|
YouTube,
|
||||||
|
Gaana,
|
||||||
|
}
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@ -31,7 +32,6 @@ data class Track(
|
|||||||
var explicit: Boolean? = null,
|
var explicit: Boolean? = null,
|
||||||
var external_urls: Map<String?, String?>? = null,
|
var external_urls: Map<String?, String?>? = null,
|
||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var id: String? = null,
|
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var preview_url: String? = null,
|
var preview_url: String? = null,
|
||||||
var track_number: Int = 0,
|
var track_number: Int = 0,
|
||||||
@ -40,5 +40,6 @@ data class Track(
|
|||||||
var album: Album? = null,
|
var album: Album? = null,
|
||||||
var external_ids: Map<String?, String?>? = null,
|
var external_ids: Map<String?, String?>? = null,
|
||||||
var popularity: Int? = null,
|
var popularity: Int? = null,
|
||||||
var ytCoverUrl:String? = null,
|
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||||
var downloaded:String? = "notDownloaded"):Parcelable
|
):Parcelable
|
||||||
|
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -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.models
|
package com.shabinder.spotiflyer.models.spotify
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.networking
|
||||||
|
|
||||||
|
import com.shabinder.spotiflyer.models.Optional
|
||||||
|
import com.shabinder.spotiflyer.models.gaana.*
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
|
||||||
|
|
||||||
|
interface GaanaInterface {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
*
|
||||||
|
* subtype : ["most_popular_playlist" , "playlist_home_featured" ,"playlist_detail" ,"user_playlist" ,"topCharts"]
|
||||||
|
**/
|
||||||
|
@GET(".")
|
||||||
|
suspend fun getGaanaPlaylist(
|
||||||
|
@Query("type") type: String = "playlist",
|
||||||
|
@Query("subtype") subtype: String = "playlist_detail",
|
||||||
|
@Query("seokey") seokey: String,
|
||||||
|
@Query("token") token: String = gaana_token,
|
||||||
|
@Query("format") format: String = "JSON",
|
||||||
|
@Query("limit") limit: Int = 2000
|
||||||
|
): Optional<GaanaPlaylist>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Api Request: http://api.gaana.com/?type=album&subtype=album_detail&seokey=kabir-singh&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
*
|
||||||
|
* subtype : ["most_popular" , "new_release" ,"featured_album" ,"similar_album" ,"all_albums", "album" ,"album_detail" ,"album_detail_info"]
|
||||||
|
**/
|
||||||
|
@GET(".")
|
||||||
|
suspend fun getGaanaAlbum(
|
||||||
|
@Query("type") type: String = "album",
|
||||||
|
@Query("subtype") subtype: String = "album_detail",
|
||||||
|
@Query("seokey") seokey: String,
|
||||||
|
@Query("token") token: String = gaana_token,
|
||||||
|
@Query("format") format: String = "JSON",
|
||||||
|
@Query("limit") limit: Int = 2000
|
||||||
|
): Optional<GaanaAlbum>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Api Request: http://api.gaana.com/?type=song&subtype=song_detail&seokey=pachtaoge&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
*
|
||||||
|
* subtype : ["most_popular" , "hot_songs" ,"recommendation" ,"song_detail"]
|
||||||
|
**/
|
||||||
|
@GET(".")
|
||||||
|
suspend fun getGaanaSong(
|
||||||
|
@Query("type") type: String = "song",
|
||||||
|
@Query("subtype") subtype: String = "song_detail",
|
||||||
|
@Query("seokey") seokey: String,
|
||||||
|
@Query("token") token: String = gaana_token,
|
||||||
|
@Query("format") format: String = "JSON",
|
||||||
|
): Optional<GaanaSong>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Api Request: https://api.gaana.com/?type=artist&subtype=artist_details_info&seokey=neha-kakkar&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
*
|
||||||
|
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
|
||||||
|
**/
|
||||||
|
@GET(".")
|
||||||
|
suspend fun getGaanaArtistDetails(
|
||||||
|
@Query("type") type: String = "artist",
|
||||||
|
@Query("subtype") subtype: String = "artist_details_info",
|
||||||
|
@Query("seokey") seokey: String,
|
||||||
|
@Query("token") token: String = gaana_token,
|
||||||
|
@Query("format") format: String = "JSON",
|
||||||
|
): Optional<GaanaArtistDetails>
|
||||||
|
/*
|
||||||
|
* Api Request: http://api.gaana.com/?type=artist&subtype=artist_track_listing&seokey=neha-kakkar&limit=50&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
*
|
||||||
|
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
|
||||||
|
**/
|
||||||
|
@GET(".")
|
||||||
|
suspend fun getGaanaArtistTracks(
|
||||||
|
@Query("type") type: String = "artist",
|
||||||
|
@Query("subtype") subtype: String = "artist_track_listing",
|
||||||
|
@Query("seokey") seokey: String,
|
||||||
|
@Query("token") token: String = gaana_token,
|
||||||
|
@Query("format") format: String = "JSON",
|
||||||
|
@Query("limit") limit: Int = 50
|
||||||
|
): Optional<GaanaArtistTracks>
|
||||||
|
|
||||||
|
}
|
@ -15,58 +15,41 @@
|
|||||||
* 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.utils
|
package com.shabinder.spotiflyer.networking
|
||||||
|
|
||||||
import com.shabinder.spotiflyer.models.*
|
import com.shabinder.spotiflyer.models.Optional
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.*
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
interface SpotifyService {
|
interface SpotifyService {
|
||||||
|
|
||||||
@GET("playlists/{playlist_id}")
|
@GET("playlists/{playlist_id}")
|
||||||
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Playlist
|
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Optional<Playlist>
|
||||||
|
|
||||||
@GET("playlists/{playlist_id}/tracks")
|
@GET("playlists/{playlist_id}/tracks")
|
||||||
suspend fun getPlaylistTracks(
|
suspend fun getPlaylistTracks(
|
||||||
@Path("playlist_id") playlistId: String?,
|
@Path("playlist_id") playlistId: String?,
|
||||||
@Query("offset") offset: Int = 0,
|
@Query("offset") offset: Int = 0,
|
||||||
@Query("limit") limit: Int = 100
|
@Query("limit") limit: Int = 100
|
||||||
): PagingObjectPlaylistTrack
|
): Optional<PagingObjectPlaylistTrack>
|
||||||
|
|
||||||
@GET("tracks/{id}")
|
@GET("tracks/{id}")
|
||||||
suspend fun getTrack(@Path("id") trackId: String?): Track
|
suspend fun getTrack(@Path("id") trackId: String?): Optional<Track>
|
||||||
|
|
||||||
@GET("episodes/{id}")
|
@GET("episodes/{id}")
|
||||||
suspend fun getEpisode(@Path("id") episodeId: String?): Track
|
suspend fun getEpisode(@Path("id") episodeId: String?): Optional<Track>
|
||||||
|
|
||||||
@GET("shows/{id}")
|
@GET("shows/{id}")
|
||||||
suspend fun getShow(@Path("id") showId: String?): Track
|
suspend fun getShow(@Path("id") showId: String?): Optional<Track>
|
||||||
|
|
||||||
@GET("albums/{id}")
|
@GET("albums/{id}")
|
||||||
suspend fun getAlbum(@Path("id") albumId: String?): Album
|
suspend fun getAlbum(@Path("id") albumId: String?): Optional<Album>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpotifyServiceTokenRequest{
|
interface SpotifyServiceTokenRequest{
|
||||||
|
|
||||||
@POST("api/token")
|
@POST("api/token")
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"):Token?
|
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"): Optional<Token>
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.networking
|
||||||
|
|
||||||
|
import com.beust.klaxon.JsonObject
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
|
||||||
|
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
|
|
||||||
|
interface YoutubeMusicApi {
|
||||||
|
|
||||||
|
@Headers("Content-Type: application/json", "Referer: https://music.youtube.com/search")
|
||||||
|
@POST("search?alt=json&key=$apiKey")
|
||||||
|
fun getYoutubeMusicResponse(@Body text: String): Call<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeJsonBody(query: String):JsonObject{
|
||||||
|
val client = JsonObject()
|
||||||
|
client["clientName"] = "WEB_REMIX"
|
||||||
|
client["clientVersion"] = "0.1"
|
||||||
|
|
||||||
|
val context = JsonObject()
|
||||||
|
context["client"] = client
|
||||||
|
|
||||||
|
val mainObject = JsonObject()
|
||||||
|
mainObject["context"] = context
|
||||||
|
mainObject["query"] = query
|
||||||
|
|
||||||
|
return mainObject
|
||||||
|
}
|
@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||||
import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding
|
import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections
|
import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.utils.bindImage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -34,30 +35,43 @@ import kotlinx.coroutines.launch
|
|||||||
class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.ViewHolder>(DownloadRecordDiffCallback()) {
|
class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.ViewHolder>(DownloadRecordDiffCallback()) {
|
||||||
|
|
||||||
private val adapterScope = CoroutineScope(Dispatchers.Default)
|
private val adapterScope = CoroutineScope(Dispatchers.Default)
|
||||||
|
//Remember To change when Submitting a Different List / Or Use New Submit List Function
|
||||||
|
var source:Source = Source.Spotify
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val layoutInflater = LayoutInflater.from(parent.context)
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
val binding =DownloadRecordItemBinding.inflate(layoutInflater)
|
val binding = DownloadRecordItemBinding.inflate(layoutInflater)
|
||||||
return ViewHolder(binding)
|
return ViewHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
bindImage(holder.binding.coverUrl,item.coverUrl)
|
bindImage(holder.binding.coverUrl,item.coverUrl,source)
|
||||||
}
|
}
|
||||||
holder.binding.itemName.text = item.name
|
holder.binding.itemName.text = item.name
|
||||||
holder.binding.totalItems.text = "Tracks: ${item.totalFiles}"
|
holder.binding.totalItems.text = "Tracks: ${item.totalFiles}"
|
||||||
holder.binding.type.text = item.type
|
holder.binding.type.text = item.type
|
||||||
holder.binding.btnAction.setOnClickListener {
|
holder.binding.btnAction.setOnClickListener {
|
||||||
if (item.link.contains("spotify",true)){
|
when {
|
||||||
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link)))
|
item.link.contains("spotify",true) -> {
|
||||||
}else if(item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) ){
|
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link)))
|
||||||
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link))
|
}
|
||||||
}
|
item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) -> {
|
||||||
|
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link))
|
||||||
|
}
|
||||||
|
item.link.contains("gaana",true) -> {
|
||||||
|
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToGaanaFragment((item.link)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ViewHolder(val binding: DownloadRecordItemBinding) : RecyclerView.ViewHolder(binding.root)
|
class ViewHolder(val binding: DownloadRecordItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
fun submitList(list: MutableList<DownloadRecord>?,source: Source) {
|
||||||
|
super.submitList(list)
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadRecordDiffCallback: DiffUtil.ItemCallback<DownloadRecord>(){
|
class DownloadRecordDiffCallback: DiffUtil.ItemCallback<DownloadRecord>(){
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.spotiflyer.recyclerView
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.context
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.downloadAllTracks
|
|
||||||
import com.shabinder.spotiflyer.models.Track
|
|
||||||
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
|
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
|
||||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(SpotifyTrackDiffCallback()) {
|
|
||||||
|
|
||||||
var spotifyViewModel : SpotifyViewModel? = null
|
|
||||||
var isAlbum:Boolean = false
|
|
||||||
var ytDownloader: YoutubeDownloader? = null
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val layoutInflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = TrackListItemBinding.inflate(layoutInflater,parent,false)
|
|
||||||
return ViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val item = getItem(position)
|
|
||||||
if(itemCount ==1 || isAlbum){
|
|
||||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
//Placeholder Set
|
|
||||||
bindImage(holder.binding.imageUrl, item.album!!.images?.get(0)?.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
|
|
||||||
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
|
|
||||||
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
|
||||||
when (item.downloaded) {
|
|
||||||
"Downloaded" -> {
|
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
|
||||||
holder.binding.btnDownload.clearAnimation()
|
|
||||||
}
|
|
||||||
"Downloading" -> {
|
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
|
||||||
rotateAnim(holder.binding.btnDownload)
|
|
||||||
}
|
|
||||||
"notDownloaded" -> {
|
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
|
||||||
holder.binding.btnDownload.clearAnimation()
|
|
||||||
holder.binding.btnDownload.setOnClickListener{
|
|
||||||
Toast.makeText(context,"Starting Download",Toast.LENGTH_SHORT).show()
|
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
|
||||||
rotateAnim(it)
|
|
||||||
item.downloaded = "Downloading"
|
|
||||||
spotifyViewModel!!.uiScope.launch {
|
|
||||||
val itemList = mutableListOf<Track>()
|
|
||||||
itemList.add(item)
|
|
||||||
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList,ytDownloader)
|
|
||||||
}
|
|
||||||
notifyItemChanged(position)//start showing anim!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpotifyTrackDiffCallback: DiffUtil.ItemCallback<Track>(){
|
|
||||||
override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean {
|
|
||||||
return oldItem.name == newItem.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean {
|
|
||||||
return oldItem == newItem //Downloaded Check
|
|
||||||
}
|
|
||||||
}
|
|
123
app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt
Executable file
123
app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt
Executable file
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.recyclerView
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.shabinder.spotiflyer.R
|
||||||
|
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
||||||
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||||
|
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()) {
|
||||||
|
|
||||||
|
var source:Source =Source.Spotify
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(
|
||||||
|
parent: ViewGroup,
|
||||||
|
viewType: Int
|
||||||
|
): ViewHolder {
|
||||||
|
val layoutInflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = TrackListItemBinding.inflate(layoutInflater,parent,false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{
|
||||||
|
viewModel.uiScope.launch {
|
||||||
|
bindImage(holder.binding.imageUrl,item.albumArtURL, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (item.downloaded) {
|
||||||
|
DownloadStatus.Downloaded -> {
|
||||||
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
|
||||||
|
holder.binding.btnDownload.clearAnimation()
|
||||||
|
}
|
||||||
|
DownloadStatus.Downloading -> {
|
||||||
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||||
|
rotateAnim(holder.binding.btnDownload)
|
||||||
|
}
|
||||||
|
DownloadStatus.NotDownloaded -> {
|
||||||
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
|
||||||
|
holder.binding.btnDownload.clearAnimation()
|
||||||
|
holder.binding.btnDownload.setOnClickListener{
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
showMessage("Processing!")
|
||||||
|
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
||||||
|
rotateAnim(it)
|
||||||
|
item.downloaded = DownloadStatus.Downloading
|
||||||
|
when(source){
|
||||||
|
Source.YouTube -> {
|
||||||
|
viewModel.uiScope.launch {
|
||||||
|
YTDownloadHelper.downloadYTTracks(
|
||||||
|
viewModel.folderType,
|
||||||
|
viewModel.subFolder,
|
||||||
|
listOf(item)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.uiScope.launch {
|
||||||
|
DownloadHelper.downloadAllTracks(
|
||||||
|
viewModel.folderType,
|
||||||
|
viewModel.subFolder,
|
||||||
|
listOf(item)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyItemChanged(position)//start showing anim!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}"
|
||||||
|
holder.binding.artist.text = "${item.artists.get(0)}..."
|
||||||
|
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
fun submitList(list: MutableList<TrackDetails>?, source: Source) {
|
||||||
|
super.submitList(list)
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class TrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){
|
||||||
|
override fun areItemsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||||
|
return oldItem.title == newItem.title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.spotiflyer.recyclerView
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
|
||||||
import com.shabinder.spotiflyer.models.Track
|
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class YoutubeTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
|
|
||||||
|
|
||||||
var format:Format? = null
|
|
||||||
private val adapterScope = CoroutineScope(Dispatchers.Default)
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): SpotifyTrackListAdapter.ViewHolder {
|
|
||||||
val layoutInflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = TrackListItemBinding.inflate(layoutInflater,parent,false)
|
|
||||||
// val view = layoutInflater.inflate(R.layout.track_list_item,parent,false)
|
|
||||||
return SpotifyTrackListAdapter.ViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SpotifyTrackListAdapter.ViewHolder, position: Int) {
|
|
||||||
val item = getItem(position)
|
|
||||||
if(itemCount == 1){
|
|
||||||
holder.binding.imageUrl.visibility = View.GONE}else{
|
|
||||||
adapterScope.launch {
|
|
||||||
bindImage(holder.binding.imageUrl, item.ytCoverUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
|
|
||||||
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
|
|
||||||
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
|
||||||
holder.binding.btnDownload.setOnClickListener{
|
|
||||||
adapterScope.launch {
|
|
||||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<Track>(){
|
|
||||||
override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean {
|
|
||||||
return oldItem.name == newItem.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
D/Retrofit: <--- HTTP 200 https://api.spotify.com/v1/me/top/artists (7170ms)
|
D/Retrofit: <--- HTTP 200 https://api.spotify.com/v1/me/top/artists (7170ms)
|
||||||
2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.Artist@4fae9ec, kaaes.spotify.webapi.android.models.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.Artist@ed6004a, kaaes.spotify.webapi.android.models.Artist@870dbbb, kaaes.spotify.webapi.android.models.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.Artist@aab431, kaaes.spotify.webapi.android.models.Artist@a7bd716, kaaes.spotify.webapi.android.models.Artist@3477897, kaaes.spotify.webapi.android.models.Artist@7f68a84]
|
2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@4fae9ec, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@ed6004a, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@870dbbb, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aab431, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@a7bd716, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@3477897, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@7f68a84]
|
||||||
|
|
||||||
|
|
||||||
I/Network: https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN
|
I/Network: https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN
|
||||||
|
@ -21,12 +21,11 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding
|
import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter
|
import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@ -41,43 +40,49 @@ class DownloadRecordFragment : Fragment() {
|
|||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = DataBindingUtil.inflate(inflater,R.layout.download_record_fragment,container,false)
|
binding = DownloadRecordFragmentBinding.inflate(inflater,container,false)
|
||||||
downloadRecordViewModel = ViewModelProvider(this).get(DownloadRecordViewModel::class.java)
|
downloadRecordViewModel = ViewModelProvider(this).get(DownloadRecordViewModel::class.java)
|
||||||
adapter = DownloadRecordAdapter()
|
adapter = DownloadRecordAdapter()
|
||||||
binding.downloadRecordList.adapter = adapter
|
binding.downloadRecordList.adapter = adapter
|
||||||
|
|
||||||
downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, {
|
downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, {
|
||||||
if(it.isNotEmpty()){
|
if(it.isNotEmpty()){
|
||||||
downloadRecordViewModel.spotifyList = mutableListOf()
|
resetLists()
|
||||||
downloadRecordViewModel.ytList = mutableListOf()
|
|
||||||
for (downloadRecord in it) {
|
for (downloadRecord in it) {
|
||||||
if(downloadRecord.link.contains("spotify",true)) downloadRecordViewModel.spotifyList.add(downloadRecord)
|
when{
|
||||||
else downloadRecordViewModel.ytList.add(downloadRecord)
|
downloadRecord.link.contains("spotify",true) -> downloadRecordViewModel.spotifyList.add(downloadRecord)
|
||||||
|
downloadRecord.link.contains("gaana",true) -> downloadRecordViewModel.gaanaList.add(downloadRecord)
|
||||||
|
else -> downloadRecordViewModel.ytList.add(downloadRecord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when(binding.tabLayout.selectedTabPosition){
|
||||||
|
0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify)
|
||||||
|
1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana)
|
||||||
|
2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube)
|
||||||
}
|
}
|
||||||
if(binding.tabLayout.selectedTabPosition == 0) adapter.submitList(downloadRecordViewModel.spotifyList)
|
|
||||||
else adapter.submitList(downloadRecordViewModel.ytList)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
if(tab?.text == "Spotify"){
|
when(tab?.position){
|
||||||
adapter.submitList(downloadRecordViewModel.spotifyList)
|
0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify)
|
||||||
} else adapter.submitList(downloadRecordViewModel.ytList)
|
1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana)
|
||||||
}
|
2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube)
|
||||||
|
}
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
|
||||||
// Handle tab reselect
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
|
||||||
// Handle tab unselected
|
|
||||||
}
|
}
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resetLists() {
|
||||||
|
downloadRecordViewModel.spotifyList = mutableListOf()
|
||||||
|
downloadRecordViewModel.ytList = mutableListOf()
|
||||||
|
downloadRecordViewModel.gaanaList = mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -32,6 +32,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data
|
|||||||
private var viewModelJob = Job()
|
private var viewModelJob = Job()
|
||||||
private val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob)
|
private val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob)
|
||||||
var spotifyList = mutableListOf<DownloadRecord>()
|
var spotifyList = mutableListOf<DownloadRecord>()
|
||||||
|
var gaanaList = mutableListOf<DownloadRecord>()
|
||||||
var ytList = mutableListOf<DownloadRecord>()
|
var ytList = mutableListOf<DownloadRecord>()
|
||||||
val downloadRecordList = MutableLiveData<MutableList<DownloadRecord>>().apply {
|
val downloadRecordList = MutableLiveData<MutableList<DownloadRecord>>().apply {
|
||||||
value = mutableListOf()
|
value = mutableListOf()
|
||||||
@ -40,6 +41,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data
|
|||||||
init {
|
init {
|
||||||
getDownloadRecordList()
|
getDownloadRecordList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloadRecordList() {
|
private fun getDownloadRecordList() {
|
||||||
uiScope.launch {
|
uiScope.launch {
|
||||||
downloadRecordList.postValue(databaseDAO.getRecord().toMutableList())
|
downloadRecordList.postValue(databaseDAO.getRecord().toMutableList())
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.ui.gaana
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
|
import com.shabinder.spotiflyer.networking.GaanaInterface
|
||||||
|
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||||
|
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
|
||||||
|
|
||||||
|
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
||||||
|
@Inject lateinit var gaanaInterface: GaanaInterface
|
||||||
|
override lateinit var viewModel: GaanaViewModel
|
||||||
|
override lateinit var adapter: TrackListAdapter
|
||||||
|
override var source: Source = Source.Gaana
|
||||||
|
override val args: GaanaFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
initializeAll()
|
||||||
|
|
||||||
|
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
|
||||||
|
//Link Schema: https://gaana.com/type/link
|
||||||
|
val link = gaanaLink.substringAfterLast('/', "error")
|
||||||
|
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
|
||||||
|
|
||||||
|
Log.i("Gaana Fragment", "$type : $link")
|
||||||
|
|
||||||
|
when{
|
||||||
|
type == "Error" || link == "Error" -> {
|
||||||
|
showMessage("Please Check Your Link!")
|
||||||
|
Provider.mainActivity.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
viewModel.gaanaSearch(type,link)
|
||||||
|
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
|
binding.downloadingFab.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
rotateAnim(binding.downloadingFab)
|
||||||
|
for (track in viewModel.trackList.value!!){
|
||||||
|
if(track.downloaded != DownloadStatus.Downloaded){
|
||||||
|
track.downloaded = DownloadStatus.Downloading
|
||||||
|
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showMessage("Processing!")
|
||||||
|
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||||
|
val urlList = arrayListOf<String>()
|
||||||
|
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
|
||||||
|
//Appending Source
|
||||||
|
urlList.add("gaana")
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
urlList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.uiScope.launch {
|
||||||
|
val finalList = viewModel.trackList.value
|
||||||
|
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
|
||||||
|
DownloadHelper.downloadAllTracks(
|
||||||
|
viewModel.folderType,
|
||||||
|
viewModel.subFolder,
|
||||||
|
finalList ?: listOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic Initialization
|
||||||
|
**/
|
||||||
|
private fun initializeAll() {
|
||||||
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
|
viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
|
||||||
|
viewModel.gaanaInterface = gaanaInterface
|
||||||
|
adapter = TrackListAdapter(viewModel)
|
||||||
|
DownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||||
|
DownloadHelper.sharedViewModel = sharedViewModel
|
||||||
|
DownloadHelper.statusBar = binding.statusBar
|
||||||
|
binding.trackList.adapter = adapter
|
||||||
|
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.ui.gaana
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.hilt.lifecycle.ViewModelInject
|
||||||
|
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||||
|
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.models.gaana.*
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
|
import com.shabinder.spotiflyer.networking.GaanaInterface
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider
|
||||||
|
import com.shabinder.spotiflyer.utils.TrackListViewModel
|
||||||
|
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
|
||||||
|
|
||||||
|
override var folderType:String = ""
|
||||||
|
override var subFolder:String = ""
|
||||||
|
var gaanaInterface : GaanaInterface? = null
|
||||||
|
val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||||
|
|
||||||
|
fun gaanaSearch(type:String,link:String){
|
||||||
|
when(type){
|
||||||
|
"song" -> {
|
||||||
|
uiScope.launch {
|
||||||
|
getGaanaSong(link)?.tracks?.firstOrNull()?.also {
|
||||||
|
folderType = "Tracks"
|
||||||
|
if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
|
}
|
||||||
|
trackList.value = listOf(it).toTrackDetailsList()
|
||||||
|
title.value = it.track_title
|
||||||
|
coverUrl.value = it.artworkLink
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
databaseDAO.insert(
|
||||||
|
DownloadRecord(
|
||||||
|
type = "Track",
|
||||||
|
name = title.value!!,
|
||||||
|
link = "https://gaana.com/$type/$link",
|
||||||
|
coverUrl = coverUrl.value!!,
|
||||||
|
totalFiles = 1,
|
||||||
|
downloaded = it.downloaded == DownloadStatus.Downloaded,
|
||||||
|
directory = finalOutputDir(it.track_title,folderType,subFolder)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"album" -> {
|
||||||
|
uiScope.launch {
|
||||||
|
getGaanaAlbum(link)?.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.value = it.tracks.toTrackDetailsList()
|
||||||
|
title.value = link
|
||||||
|
coverUrl.value = it.custom_artworks.size_480p
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
databaseDAO.insert(DownloadRecord(
|
||||||
|
type = "Album",
|
||||||
|
name = title.value!!,
|
||||||
|
link = "https://gaana.com/$type/$link",
|
||||||
|
coverUrl = coverUrl.value.toString(),
|
||||||
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
||||||
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"playlist" -> {
|
||||||
|
uiScope.launch {
|
||||||
|
getGaanaPlaylist(link)?.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.value = it.tracks.toTrackDetailsList()
|
||||||
|
title.value = link
|
||||||
|
//coverUrl.value = "TODO"
|
||||||
|
coverUrl.value = gaanaPlaceholderImageUrl
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
databaseDAO.insert(DownloadRecord(
|
||||||
|
type = "Playlist",
|
||||||
|
name = title.value.toString(),
|
||||||
|
link = "https://gaana.com/$type/$link",
|
||||||
|
coverUrl = coverUrl.value.toString(),
|
||||||
|
totalFiles = it.tracks.size,
|
||||||
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
||||||
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"artist" -> {
|
||||||
|
uiScope.launch {
|
||||||
|
folderType = "Artist"
|
||||||
|
subFolder = link
|
||||||
|
val artistDetails = getGaanaArtistDetails(link)?.artist?.firstOrNull()?.also {
|
||||||
|
title.value = it.name
|
||||||
|
coverUrl.value = it.artworkLink
|
||||||
|
}
|
||||||
|
getGaanaArtistTracks(link)?.also {
|
||||||
|
it.tracks.forEach {track ->
|
||||||
|
if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
|
track.downloaded = DownloadStatus.Downloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackList.value = it.tracks.toTrackDetailsList()
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
databaseDAO.insert(DownloadRecord(
|
||||||
|
type = "Artist",
|
||||||
|
name = artistDetails?.name ?: link,
|
||||||
|
link = "https://gaana.com/$type/$link",
|
||||||
|
coverUrl = coverUrl.value.toString(),
|
||||||
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
||||||
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun List<GaanaTrack>.toTrackDetailsList() = this.map {
|
||||||
|
TrackDetails(
|
||||||
|
title = it.track_title,
|
||||||
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
|
durationSec = it.duration,
|
||||||
|
albumArt = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
Provider.defaultDir +".Images/" + (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
|
||||||
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
|
private suspend fun getGaanaSong(songLink:String): GaanaSong?{
|
||||||
|
Log.i("Requesting","https://gaana.com/song/$songLink")
|
||||||
|
return gaanaInterface?.getGaanaSong(seokey = songLink)?.value
|
||||||
|
}
|
||||||
|
private suspend fun getGaanaAlbum(albumLink:String): GaanaAlbum?{
|
||||||
|
Log.i("Requesting","https://gaana.com/album/$albumLink")
|
||||||
|
return gaanaInterface?.getGaanaAlbum(seokey = albumLink)?.value
|
||||||
|
}
|
||||||
|
private suspend fun getGaanaPlaylist(link:String): GaanaPlaylist?{
|
||||||
|
Log.i("Requesting","https://gaana.com/playlist/$link")
|
||||||
|
return gaanaInterface?.getGaanaPlaylist(seokey = link)?.value
|
||||||
|
}
|
||||||
|
private suspend fun getGaanaArtistDetails(link:String): GaanaArtistDetails?{
|
||||||
|
Log.i("Requesting","https://gaana.com/artist/$link")
|
||||||
|
return gaanaInterface?.getGaanaArtistDetails(seokey = link)?.value
|
||||||
|
}
|
||||||
|
private suspend fun getGaanaArtistTracks(link:String,limit:Int = 50): GaanaArtistTracks?{
|
||||||
|
Log.i("Requesting","Tracks of: https://gaana.com/artist/$link")
|
||||||
|
return gaanaInterface?.getGaanaArtistTracks(seokey = link,limit = limit)?.value
|
||||||
|
}
|
||||||
|
}
|
@ -17,22 +17,19 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.ui.mainfragment
|
package com.shabinder.spotiflyer.ui.mainfragment
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -53,123 +50,88 @@ class MainFragment : Fragment() {
|
|||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
binding = MainFragmentBinding.inflate(inflater,container,false)
|
||||||
initializeAll()
|
initializeAll()
|
||||||
|
|
||||||
binding.btnSearch.setOnClickListener {
|
binding.btnSearch.setOnClickListener {
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
val link = binding.linkSearch.text.toString()
|
val link = binding.linkSearch.text.toString()
|
||||||
if (link.contains("spotify",true)){
|
when{
|
||||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link))
|
//SPOTIFY
|
||||||
}else if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){
|
link.contains("spotify",true) -> {
|
||||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
|
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
||||||
}else{Toast.makeText(context,"Link is Not Valid",Toast.LENGTH_SHORT).show()}
|
(activity as MainActivity).authenticateSpotify()
|
||||||
|
}
|
||||||
|
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
//YOUTUBE
|
||||||
|
link.contains("youtube.com",true) || link.contains("youtu.be",true) -> {
|
||||||
|
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
//GAANA
|
||||||
|
link.contains("gaana",true) -> {
|
||||||
|
findNavController().navigate(MainFragmentDirections.actionMainFragmentToGaanaFragment(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> showMessage("Link is Not Valid",true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleIntent()
|
handleIntent()
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun initializeAll() {
|
|
||||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
|
||||||
openYTButton()
|
|
||||||
openSpotifyButton()
|
|
||||||
openGithubButton()
|
|
||||||
openInstaButton()
|
|
||||||
openLinkedInButton()
|
|
||||||
historyButton()
|
|
||||||
binding.usage.text = usageText()
|
|
||||||
binding.btnDonate.setOnClickListener {
|
|
||||||
easyUpiPayment.startPayment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun historyButton() {
|
|
||||||
binding.btnHistory.setOnClickListener {
|
|
||||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Intent If there is any!
|
* Handle Intent If there is any!
|
||||||
**/
|
**/
|
||||||
private fun handleIntent() {
|
private fun handleIntent() {
|
||||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
|
||||||
if(it != ""){
|
sharedViewModel.uiScope.launch(Dispatchers.IO) {
|
||||||
sharedViewModel.uiScope.launch(Dispatchers.IO) {
|
//Wait for any Authentication to Finish ,
|
||||||
while (sharedViewModel.accessToken.value == "") {
|
// this Wait prevents from multiple Authentication Requests
|
||||||
|
Thread.sleep(1000)
|
||||||
|
if(sharedViewModel.spotifyService.value == null){
|
||||||
|
//Not Authenticated Yet
|
||||||
|
Provider.mainActivity.authenticateSpotify()
|
||||||
|
while (sharedViewModel.spotifyService.value == null) {
|
||||||
//Waiting for Authentication to Finish
|
//Waiting for Authentication to Finish
|
||||||
Thread.sleep(1000)
|
Thread.sleep(1000)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main){
|
}
|
||||||
binding.linkSearch.setText(sharedViewModel.intentString.value)
|
|
||||||
binding.btnSearch.performClick()
|
withContext(Dispatchers.Main){
|
||||||
sharedViewModel.intentString.value = ""
|
binding.linkSearch.setText(sharedViewModel.intentString.value)
|
||||||
}
|
binding.btnSearch.performClick()
|
||||||
|
//Intent Consumed
|
||||||
|
sharedViewModel.intentString.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun initializeAll() {
|
||||||
* Implementing buttons
|
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||||
**/
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
private fun openSpotifyButton() {
|
binding.apply {
|
||||||
val manager: PackageManager = requireActivity().packageManager
|
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
|
||||||
try {
|
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
|
||||||
val i = manager.getLaunchIntentForPackage("com.spotify.music")
|
btnYoutube.openPlatformOnClick("com.google.android.youtube","http://m.youtube.com")
|
||||||
?: throw PackageManager.NameNotFoundException()
|
btnGithub.openPlatformOnClick("http://github.com/Shabinder/SpotiFlyer")
|
||||||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
btnInsta.openPlatformOnClick("http://www.instagram.com/mr.shabinder")
|
||||||
binding.btnSpotify.setOnClickListener { startActivity(i) }
|
btnHistory.setOnClickListener {
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
|
||||||
val uri: Uri =
|
}
|
||||||
Uri.parse("http://open.spotify.com")
|
usage.text = usageText()
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
btnDonate.setOnClickListener {
|
||||||
binding.btnSpotify.setOnClickListener {
|
easyUpiPayment.startPayment()
|
||||||
startActivity(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun openYTButton() {
|
|
||||||
val manager: PackageManager = requireActivity().packageManager
|
|
||||||
try {
|
|
||||||
val i = manager.getLaunchIntentForPackage("com.google.android.youtube")
|
|
||||||
?: throw PackageManager.NameNotFoundException()
|
|
||||||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
|
||||||
binding.btnYoutube.setOnClickListener { startActivity(i) }
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
val uri: Uri =
|
|
||||||
Uri.parse("http://m.youtube.com")
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
binding.btnYoutube.setOnClickListener {
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun openGithubButton() {
|
|
||||||
val uri: Uri =
|
|
||||||
Uri.parse("http://github.com/Shabinder/SpotiFlyer")
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
binding.btnGithubSpotify.setOnClickListener {
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun openLinkedInButton() {
|
|
||||||
val uri: Uri =
|
|
||||||
Uri.parse("https://in.linkedin.com/in/shabinder")
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
binding.btnLinkedin.setOnClickListener {
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun openInstaButton() {
|
|
||||||
val uri: Uri =
|
|
||||||
Uri.parse("http://www.instagram.com/mr.shabinder")
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
||||||
binding.developerInstaSpotify.setOnClickListener {
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun usageText(): SpannableStringBuilder {
|
private fun usageText(): SpannableStringBuilder {
|
||||||
return SpannableStringBuilder()
|
return SpannableStringBuilder()
|
||||||
.append(getText(R.string.d_one)).append("\n")
|
.append(getText(R.string.d_one)).append("\n")
|
||||||
|
@ -18,307 +18,124 @@
|
|||||||
package com.shabinder.spotiflyer.ui.spotify
|
package com.shabinder.spotiflyer.ui.spotify
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.bumptech.glide.Glide
|
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import com.shabinder.spotiflyer.MainActivity
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
|
||||||
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
|
||||||
import com.shabinder.spotiflyer.models.Track
|
|
||||||
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
|
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
|
||||||
import com.shabinder.spotiflyer.utils.copyTo
|
|
||||||
import com.shabinder.spotiflyer.utils.rotateAnim
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SpotifyFragment : Fragment() {
|
class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() {
|
||||||
private lateinit var binding:SpotifyFragmentBinding
|
|
||||||
private lateinit var spotifyViewModel: SpotifyViewModel
|
|
||||||
private lateinit var sharedViewModel: SharedViewModel
|
|
||||||
private lateinit var adapterSpotify:SpotifyTrackListAdapter
|
|
||||||
@Inject lateinit var ytDownloader:YoutubeDownloader
|
|
||||||
private var webView: WebView? = null
|
|
||||||
private var intentFilter:IntentFilter? = null
|
|
||||||
private var updateUIReceiver: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
|
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
||||||
|
override lateinit var viewModel: SpotifyViewModel
|
||||||
|
override lateinit var adapter: TrackListAdapter
|
||||||
|
override var source: Source = Source.Spotify
|
||||||
|
override val args: SpotifyFragmentArgs by navArgs()
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = DataBindingUtil.inflate(inflater,R.layout.spotify_fragment,container,false)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
adapterSpotify = SpotifyTrackListAdapter()
|
|
||||||
initializeAll()
|
initializeAll()
|
||||||
initializeLiveDataObservers()
|
|
||||||
initializeBroadcast()
|
|
||||||
|
|
||||||
val args = SpotifyFragmentArgs.fromBundle(requireArguments())
|
val spotifyLink = args.link.substringAfter("open.spotify.com/")
|
||||||
val spotifyLink = args.link
|
|
||||||
|
|
||||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||||
|
|
||||||
Log.i("Fragment", "$type : $link")
|
Log.i("Spotify Fragment", "$type : $link")
|
||||||
|
|
||||||
|
|
||||||
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
||||||
(activity as MainActivity).authenticateSpotify()
|
if(isOnline()) mainActivity.authenticateSpotify()
|
||||||
}
|
}
|
||||||
if(!isOnline()){//Device Offline
|
|
||||||
sharedViewModel.showAlertDialog(resources,requireContext())
|
when{
|
||||||
}else if (type == "Error" || link == "Error") {//Incorrect Link
|
type == "Error" || link == "Error" -> {
|
||||||
showToast("Please Check Your Link!")
|
showMessage("Please Check Your Link!")
|
||||||
}else if(spotifyLink.contains("open.spotify",true)){//Link Validation!!
|
mainActivity.onBackPressed()
|
||||||
if(type == "episode" || type == "show"){//TODO Implementation
|
|
||||||
showToast("Implementing Soon, Stay Tuned!")
|
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
spotifyViewModel.spotifySearch(type,link)
|
|
||||||
if(type=="album")adapterSpotify.isAlbum = true
|
|
||||||
|
|
||||||
binding.btnDownloadAllSpotify.setOnClickListener {
|
else -> {
|
||||||
for (track in spotifyViewModel.trackList.value!!){
|
if(type == "episode" || type == "show"){//TODO Implementation
|
||||||
if(track.downloaded != "Downloaded"){
|
showMessage("Implementing Soon, Stay Tuned!")
|
||||||
track.downloaded = "Downloading"
|
}
|
||||||
|
else{
|
||||||
|
this.viewModel.spotifySearch(type,link)
|
||||||
|
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
}
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
binding.downloadingFab.visibility = View.VISIBLE
|
||||||
binding.downloadingFabSpotify.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
|
rotateAnim(binding.downloadingFab)
|
||||||
rotateAnim(binding.downloadingFabSpotify)
|
for (track in this.viewModel.trackList.value ?: listOf()){
|
||||||
for (track in spotifyViewModel.trackList.value!!){
|
if(track.downloaded != DownloadStatus.Downloaded){
|
||||||
if(track.downloaded != "Downloaded"){
|
track.downloaded = DownloadStatus.Downloading
|
||||||
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track))
|
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showMessage("Processing!")
|
||||||
|
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||||
|
val urlList = arrayListOf<String>()
|
||||||
|
this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
|
||||||
|
//Appending Source
|
||||||
|
urlList.add("spotify")
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
urlList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.viewModel.uiScope.launch {
|
||||||
|
val finalList = viewModel.trackList.value
|
||||||
|
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
|
||||||
|
DownloadHelper.downloadAllTracks(
|
||||||
|
viewModel.folderType,
|
||||||
|
viewModel.subFolder,
|
||||||
|
finalList ?: listOf(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
showToast("Starting Download in Few Seconds")
|
|
||||||
spotifyViewModel.uiScope.launch(Dispatchers.Default){loadAllImages(spotifyViewModel.trackList.value!!)}
|
|
||||||
spotifyViewModel.uiScope.launch {
|
|
||||||
SpotifyDownloadHelper.downloadAllTracks(
|
|
||||||
spotifyViewModel.folderType,
|
|
||||||
spotifyViewModel.subFolder,
|
|
||||||
spotifyViewModel.trackList.value!!,
|
|
||||||
ytDownloader
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
initializeBroadcast()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initializeBroadcast() {
|
|
||||||
intentFilter = IntentFilter()
|
|
||||||
intentFilter?.addAction("track_download_completed")
|
|
||||||
|
|
||||||
updateUIReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
//UI update here
|
|
||||||
if (intent != null){
|
|
||||||
val track = intent.getParcelableExtra<Track?>("track")
|
|
||||||
track?.let {
|
|
||||||
val position: Int = spotifyViewModel.trackList.value?.indexOf(track)!!
|
|
||||||
Log.i("Track","Download Completed Intent :$position")
|
|
||||||
track.downloaded = "Downloaded"
|
|
||||||
if(position != -1) {
|
|
||||||
spotifyViewModel.trackList.value?.set(position, track)
|
|
||||||
adapterSpotify.notifyItemChanged(position)
|
|
||||||
checkIfAllDownloaded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
requireActivity().unregisterReceiver(updateUIReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*Live Data Observers
|
|
||||||
**/
|
|
||||||
private fun initializeLiveDataObservers() {
|
|
||||||
/**
|
|
||||||
* CoverUrl Binding Observer!
|
|
||||||
**/
|
|
||||||
spotifyViewModel.coverUrl.observe(viewLifecycleOwner, {
|
|
||||||
if(it!="Loading") bindImage(binding.spotifyCoverImage,it)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TrackList Binding Observer!
|
|
||||||
**/
|
|
||||||
spotifyViewModel.trackList.observe(viewLifecycleOwner, {
|
|
||||||
if (it.isNotEmpty()){
|
|
||||||
Log.i("SpotifyFragment","TrackList Updated")
|
|
||||||
adapterConfig(it)
|
|
||||||
checkIfAllDownloaded()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title Binding Observer!
|
|
||||||
**/
|
|
||||||
spotifyViewModel.title.observe(viewLifecycleOwner, {
|
|
||||||
binding.titleViewSpotify.text = it
|
|
||||||
})
|
|
||||||
|
|
||||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
|
||||||
//Waiting for Authentication to Finish with Spotify()Access Token Observe
|
|
||||||
if(it != "" && it!=SpotifyFragmentArgs.fromBundle(requireArguments()).link){
|
|
||||||
//New Intent Received , Time TO RELOAD
|
|
||||||
(activity as MainActivity).onBackPressed()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkIfAllDownloaded() {
|
|
||||||
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != "Downloaded" }){
|
|
||||||
//All Tracks Downloaded
|
|
||||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
|
||||||
binding.downloadingFabSpotify.apply{
|
|
||||||
setImageResource(R.drawable.ic_tick)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
clearAnimation()
|
|
||||||
keepScreenOn = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic Initialization
|
* Basic Initialization
|
||||||
**/
|
**/
|
||||||
private fun initializeAll() {
|
private fun initializeAll() {
|
||||||
webView = binding.webViewSpotify
|
this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
adapter = TrackListAdapter(this.viewModel)
|
||||||
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
|
||||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
this.viewModel.spotifyService = it
|
||||||
spotifyViewModel.spotifyService = it
|
|
||||||
})
|
})
|
||||||
SpotifyDownloadHelper.webView = binding.webViewSpotify
|
DownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||||
SpotifyDownloadHelper.context = requireContext()
|
DownloadHelper.sharedViewModel = sharedViewModel
|
||||||
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
DownloadHelper.statusBar = binding.statusBar
|
||||||
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
binding.trackList.adapter = adapter
|
||||||
binding.trackListSpotify.adapter = adapterSpotify
|
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
(binding.trackListSpotify.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to fetch all Images for using in mp3 tag.
|
|
||||||
**/
|
|
||||||
private suspend fun loadAllImages(trackList: List<Track>) {
|
|
||||||
trackList.forEach {
|
|
||||||
val imgUrl = it.album?.images?.get(0)?.url
|
|
||||||
imgUrl?.let {
|
|
||||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
|
||||||
Glide
|
|
||||||
.with(requireContext())
|
|
||||||
.asFile()
|
|
||||||
.load(imgUri)
|
|
||||||
.listener(object: RequestListener<File> {
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Log.i("Glide","LoadFailed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: File?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
sharedViewModel.uiScope.launch {
|
|
||||||
withContext(Dispatchers.IO){
|
|
||||||
try {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
|
|
||||||
)
|
|
||||||
resource?.copyTo(file)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}).submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure Recycler View Adapter
|
|
||||||
**/
|
|
||||||
private fun adapterConfig(trackList: List<Track>){
|
|
||||||
adapterSpotify.ytDownloader = ytDownloader
|
|
||||||
adapterSpotify.spotifyViewModel = spotifyViewModel
|
|
||||||
adapterSpotify.submitList(trackList)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util. Function to create toasts!
|
|
||||||
**/
|
|
||||||
private fun showToast(message:String){
|
|
||||||
Toast.makeText(context,message,Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util. Function To Check Connection Status
|
|
||||||
**/
|
|
||||||
private fun isOnline(): Boolean {
|
|
||||||
val cm =
|
|
||||||
requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val netInfo = cm.activeNetworkInfo
|
|
||||||
return netInfo != null && netInfo.isConnectedOrConnecting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,57 +17,53 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.ui.spotify
|
package com.shabinder.spotiflyer.ui.spotify
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.hilt.lifecycle.ViewModelInject
|
import androidx.hilt.lifecycle.ViewModelInject
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
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.*
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.*
|
||||||
|
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider
|
||||||
|
import com.shabinder.spotiflyer.utils.TrackListViewModel
|
||||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
|
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
|
||||||
ViewModel(){
|
|
||||||
|
override var folderType:String = ""
|
||||||
|
override var subFolder:String = ""
|
||||||
|
|
||||||
var folderType:String = ""
|
|
||||||
var subFolder:String = ""
|
|
||||||
var trackList = MutableLiveData<MutableList<Track>>()
|
|
||||||
private val loading = "Loading"
|
|
||||||
var title = MutableLiveData<String>().apply { value = loading }
|
|
||||||
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
|
||||||
var spotifyService : SpotifyService? = null
|
var spotifyService : SpotifyService? = null
|
||||||
|
|
||||||
private var viewModelJob = Job()
|
|
||||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
|
||||||
|
|
||||||
|
|
||||||
fun spotifySearch(type:String,link: String){
|
fun spotifySearch(type:String,link: String){
|
||||||
when (type) {
|
when (type) {
|
||||||
"track" -> {
|
"track" -> {
|
||||||
uiScope.launch {
|
uiScope.launch {
|
||||||
val trackObject = getTrackDetails(link)
|
getTrackDetails(link)?.also {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
val tempTrackList = mutableListOf<Track>()
|
if(File(finalOutputDir(it.name,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
trackObject.downloaded = "Downloaded"
|
}
|
||||||
}
|
trackList.value = listOf(it).toTrackDetailsList()
|
||||||
tempTrackList.add(trackObject)
|
title.value = it.name
|
||||||
trackList.value = tempTrackList
|
coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url
|
||||||
title.value = trackObject.name
|
withContext(Dispatchers.IO){
|
||||||
coverUrl.value = trackObject.album!!.images?.get(0)!!.url!!
|
databaseDAO.insert(DownloadRecord(
|
||||||
withContext(Dispatchers.IO){
|
type = "Track",
|
||||||
databaseDAO.insert(DownloadRecord(
|
name = title.value!!,
|
||||||
type = "Track",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
name = title.value!!,
|
coverUrl = coverUrl.value!!,
|
||||||
link = "https://open.spotify.com/$type/$link",
|
totalFiles = 1,
|
||||||
coverUrl = coverUrl.value!!,
|
downloaded = it.downloaded == DownloadStatus.Downloaded,
|
||||||
totalFiles = tempTrackList.size,
|
directory = finalOutputDir(it.name,folderType,subFolder)
|
||||||
downloaded = trackObject.downloaded =="Downloaded",
|
))
|
||||||
directory = finalOutputDir(trackObject.name!!,folderType,subFolder)
|
}
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,25 +73,23 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
val albumObject = getAlbumDetails(link)
|
val albumObject = getAlbumDetails(link)
|
||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = albumObject?.name.toString()
|
subFolder = albumObject?.name.toString()
|
||||||
val tempTrackList = mutableListOf<Track>()
|
|
||||||
albumObject?.tracks?.items?.forEach {
|
albumObject?.tracks?.items?.forEach {
|
||||||
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
it.downloaded = "Downloaded"
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
}
|
}
|
||||||
it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url)))
|
it.album = Album(images = listOf(Image(url = albumObject.images?.elementAtOrNull(1)?.url ?: albumObject.images?.elementAtOrNull(0)?.url )))
|
||||||
tempTrackList.add(it)
|
|
||||||
}
|
}
|
||||||
trackList.value = tempTrackList
|
trackList.value = albumObject?.tracks?.items?.toTrackDetailsList()
|
||||||
title.value = albumObject?.name
|
title.value = albumObject?.name
|
||||||
coverUrl.value = albumObject?.images?.get(0)?.url
|
coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url ?: albumObject?.images?.elementAtOrNull(0)?.url
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
databaseDAO.insert(DownloadRecord(
|
databaseDAO.insert(DownloadRecord(
|
||||||
type = "Album",
|
type = "Album",
|
||||||
name = title.value!!,
|
name = title.value!!,
|
||||||
link = "https://open.spotify.com/$type/$link",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
coverUrl = coverUrl.value.toString(),
|
coverUrl = coverUrl.value.toString(),
|
||||||
totalFiles = tempTrackList.size,
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -112,7 +106,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
playlistObject?.tracks?.items?.forEach {
|
playlistObject?.tracks?.items?.forEach {
|
||||||
it.track?.let {
|
it.track?.let {
|
||||||
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||||
it1.downloaded = "Downloaded"
|
it1.downloaded = DownloadStatus.Downloaded
|
||||||
}
|
}
|
||||||
tempTrackList.add(it1)
|
tempTrackList.add(it1)
|
||||||
}
|
}
|
||||||
@ -128,15 +122,15 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
|
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
|
||||||
}
|
}
|
||||||
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
||||||
trackList.value = tempTrackList
|
trackList.value = tempTrackList.toTrackDetailsList()
|
||||||
title.value = playlistObject?.name
|
title.value = playlistObject?.name
|
||||||
coverUrl.value = playlistObject?.images?.get(0)!!.url!!
|
coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url ?: playlistObject?.images?.firstOrNull()?.url.toString()
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
databaseDAO.insert(DownloadRecord(
|
databaseDAO.insert(DownloadRecord(
|
||||||
type = "Playlist",
|
type = "Playlist",
|
||||||
name = title.value!!,
|
name = title.value.toString(),
|
||||||
link = "https://open.spotify.com/$type/$link",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
coverUrl = coverUrl.value!!,
|
coverUrl = coverUrl.value.toString(),
|
||||||
totalFiles = tempTrackList.size,
|
totalFiles = tempTrackList.size,
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||||
@ -151,26 +145,39 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun List<Track>.toTrackDetailsList() = this.map {
|
||||||
|
TrackDetails(
|
||||||
|
title = it.name.toString(),
|
||||||
|
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||||
|
durationSec = (it.duration_ms/1000).toInt(),
|
||||||
|
albumArt = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
Provider.defaultDir +".Images/" + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
|
||||||
|
albumName = it.album?.name,
|
||||||
|
year = it.album?.release_date,
|
||||||
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
|
trackUrl = it.href,
|
||||||
|
downloaded = it.downloaded,
|
||||||
|
source = Source.Spotify,
|
||||||
|
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()
|
||||||
|
)
|
||||||
|
}.toMutableList()
|
||||||
|
|
||||||
private suspend fun getTrackDetails(trackLink:String): Track?{
|
private suspend fun getTrackDetails(trackLink:String): Track?{
|
||||||
Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink")
|
Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink")
|
||||||
return spotifyService?.getTrack(trackLink)
|
return spotifyService?.getTrack(trackLink)?.value
|
||||||
}
|
}
|
||||||
private suspend fun getAlbumDetails(albumLink:String): Album?{
|
private suspend fun getAlbumDetails(albumLink:String): Album?{
|
||||||
Log.i("Requesting","https://api.spotify.com/v1/albums/$albumLink")
|
Log.i("Requesting","https://api.spotify.com/v1/albums/$albumLink")
|
||||||
return spotifyService?.getAlbum(albumLink)
|
return spotifyService?.getAlbum(albumLink)?.value
|
||||||
}
|
}
|
||||||
private suspend fun getPlaylistDetails(link:String): Playlist?{
|
private suspend fun getPlaylistDetails(link:String): Playlist?{
|
||||||
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link")
|
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link")
|
||||||
return spotifyService?.getPlaylist(link)
|
return spotifyService?.getPlaylist(link)?.value
|
||||||
}
|
}
|
||||||
private suspend fun getPlaylistTrackDetails(link:String,offset:Int = 0,limit:Int = 100): PagingObjectPlaylistTrack?{
|
private suspend fun getPlaylistTrackDetails(link:String,offset:Int = 0,limit:Int = 100): PagingObjectPlaylistTrack?{
|
||||||
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit")
|
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit")
|
||||||
return spotifyService?.getPlaylistTracks(link, offset, limit)
|
return spotifyService?.getPlaylistTracks(link, offset, limit)?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
viewModelJob.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -21,46 +21,39 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.databinding.DataBindingUtil
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.SharedViewModel
|
|
||||||
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import com.shabinder.spotiflyer.utils.bindImage
|
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||||
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
private const val sampleDomain2 = "youtu.be"
|
||||||
class YoutubeFragment : Fragment() {
|
private const val sampleDomain1 = "youtube.com"
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>() {
|
||||||
|
|
||||||
private lateinit var binding:YoutubeFragmentBinding
|
|
||||||
private lateinit var youtubeViewModel: YoutubeViewModel
|
|
||||||
private lateinit var sharedViewModel: SharedViewModel
|
|
||||||
private lateinit var adapter : YoutubeTrackListAdapter
|
|
||||||
private val sampleDomain1 = "youtube.com"
|
|
||||||
private val sampleDomain2 = "youtu.be"
|
|
||||||
@Inject lateinit var ytDownloader: YoutubeDownloader
|
@Inject lateinit var ytDownloader: YoutubeDownloader
|
||||||
|
override lateinit var viewModel: YoutubeViewModel
|
||||||
|
override lateinit var adapter : TrackListAdapter
|
||||||
|
override var source: Source = Source.YouTube
|
||||||
|
override val args: YoutubeFragmentArgs by navArgs()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
adapter = TrackListAdapter(this.viewModel)
|
||||||
adapter = YoutubeTrackListAdapter()
|
binding.trackList.adapter = adapter
|
||||||
YTDownloadHelper.context = requireContext()
|
|
||||||
YTDownloadHelper.statusBar = binding.StatusBarYoutube
|
|
||||||
binding.trackListYoutube.adapter = adapter
|
|
||||||
|
|
||||||
initializeLiveDataObservers()
|
|
||||||
|
|
||||||
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
|
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
|
||||||
val link = args.link
|
val link = args.link
|
||||||
@ -70,7 +63,11 @@ class YoutubeFragment : Fragment() {
|
|||||||
|
|
||||||
private fun youtubeSearch(linkSearch:String) {
|
private fun youtubeSearch(linkSearch:String) {
|
||||||
val link = linkSearch.removePrefix("https://").removePrefix("http://")
|
val link = linkSearch.removePrefix("https://").removePrefix("http://")
|
||||||
if(!link.contains("playlist",true)){
|
if(link.contains("playlist",true) || link.contains("list",true)){
|
||||||
|
// Given Link is of a Playlist
|
||||||
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
|
||||||
|
this.viewModel.getYTPlaylist(playlistId,ytDownloader)
|
||||||
|
}else{//Given Link is of a Video
|
||||||
var searchId = "error"
|
var searchId = "error"
|
||||||
if(link.contains(sampleDomain1,true) ){
|
if(link.contains(sampleDomain1,true) ){
|
||||||
searchId = link.substringAfterLast("=","error")
|
searchId = link.substringAfterLast("=","error")
|
||||||
@ -79,57 +76,48 @@ class YoutubeFragment : Fragment() {
|
|||||||
searchId = link.substringAfterLast("/","error")
|
searchId = link.substringAfterLast("/","error")
|
||||||
}
|
}
|
||||||
if(searchId != "error") {
|
if(searchId != "error") {
|
||||||
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
this.viewModel.getYTTrack(searchId,ytDownloader)
|
||||||
binding.btnDownloadAllYoutube.setOnClickListener {
|
}else{showMessage("Your Youtube Link is not of a Video!!")}
|
||||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",
|
}
|
||||||
youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value)
|
|
||||||
|
/*
|
||||||
|
* Download All Tracks
|
||||||
|
* */
|
||||||
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
|
binding.downloadingFab.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
rotateAnim(binding.downloadingFab)
|
||||||
|
|
||||||
|
for (track in this.viewModel.trackList.value?: listOf()){
|
||||||
|
if(track.downloaded != DownloadStatus.Downloaded){
|
||||||
|
track.downloaded = DownloadStatus.Downloading
|
||||||
|
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
||||||
}
|
}
|
||||||
}else{showToast("Your Youtube Link is not of a Video!!")}
|
}
|
||||||
}else(showToast("Your Youtube Link is not of a Video!!"))
|
showMessage("Processing!")
|
||||||
|
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
||||||
|
val urlList = arrayListOf<String>()
|
||||||
|
viewModel.trackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
|
||||||
|
.substringBeforeLast(".")}/hqdefault.jpg")}
|
||||||
|
//Appending Source
|
||||||
|
urlList.add("youtube")
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
urlList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.uiScope.launch {
|
||||||
|
YTDownloadHelper.downloadYTTracks(
|
||||||
|
type = viewModel.folderType,
|
||||||
|
subFolder = viewModel.subFolder,
|
||||||
|
tracks = viewModel.trackList.value ?: listOf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeLiveDataObservers() {
|
|
||||||
/**
|
|
||||||
* CoverUrl Binding Observer!
|
|
||||||
**/
|
|
||||||
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, Observer {
|
|
||||||
if(it!="Loading") bindImage(binding.youtubeCoverImage,it)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TrackList Binding Observer!
|
|
||||||
**/
|
|
||||||
youtubeViewModel.ytTrack.observe(viewLifecycleOwner, Observer {
|
|
||||||
val list = mutableListOf<Track>()
|
|
||||||
list.add(it)
|
|
||||||
adapterConfig(list)
|
|
||||||
})
|
|
||||||
|
|
||||||
youtubeViewModel.format.observe(viewLifecycleOwner, Observer {
|
|
||||||
adapter.format = it
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Title Binding Observer!
|
|
||||||
**/
|
|
||||||
youtubeViewModel.title.observe(viewLifecycleOwner, Observer {
|
|
||||||
binding.titleViewYoutube.text = it
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure Recycler View Adapter
|
|
||||||
**/
|
|
||||||
private fun adapterConfig(list:List<Track>){
|
|
||||||
adapter.submitList(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Util. Function to create toasts!
|
|
||||||
**/
|
|
||||||
private fun showToast(message:String){
|
|
||||||
Toast.makeText(context,message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,79 +17,134 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.ui.youtube
|
package com.shabinder.spotiflyer.ui.youtube
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.hilt.lifecycle.ViewModelInject
|
import androidx.hilt.lifecycle.ViewModelInject
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
|
||||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
|
||||||
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.Artist
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
import kotlinx.coroutines.*
|
import com.shabinder.spotiflyer.utils.*
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
|
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
|
||||||
ViewModel(){
|
/*
|
||||||
|
* 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"
|
||||||
|
* */
|
||||||
|
|
||||||
val ytTrack = MutableLiveData<Track>()
|
override var folderType = "YT_Downloads"
|
||||||
val format = MutableLiveData<Format>()
|
override var subFolder = ""
|
||||||
private val loading = "Loading"
|
|
||||||
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
|
|
||||||
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
|
||||||
|
|
||||||
private var viewModelJob = Job()
|
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
|
||||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
if(!isOnline())return
|
||||||
|
try{
|
||||||
|
uiScope.launch(Dispatchers.IO) {
|
||||||
fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) {
|
Log.i("YT Playlist",searchId)
|
||||||
uiScope.launch {
|
val playlist = ytDownloader.getPlaylist(searchId)
|
||||||
withContext(Dispatchers.IO){
|
val playlistDetails = playlist.details()
|
||||||
Log.i("YT View Model",searchId)
|
val name = playlistDetails.title()
|
||||||
val video = ytDownloader.getVideo(searchId)
|
subFolder = removeIllegalChars(name).toString()
|
||||||
val detail = video?.details()
|
val videos = playlist.videos()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title()
|
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg")
|
||||||
Log.i("YT View Model",detail.toString())
|
|
||||||
ytTrack.postValue(
|
|
||||||
Track(
|
|
||||||
id = searchId,
|
|
||||||
name = name,
|
|
||||||
artists = listOf<Artist>(Artist(name = detail?.author())),
|
|
||||||
duration_ms = detail?.lengthSeconds()?.times(1000)?.toLong()?:0,
|
|
||||||
ytCoverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
|
||||||
))
|
|
||||||
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg")
|
|
||||||
title.postValue(
|
title.postValue(
|
||||||
if(name?.length!! > 17){"${name.subSequence(0,16)}..."}else{name}
|
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||||
)
|
)
|
||||||
format.postValue(try {
|
this@YoutubeViewModel.trackList.postValue(videos.map {
|
||||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
TrackDetails(
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
title = it.title(),
|
||||||
try {
|
artists = listOf(it.author().toString()),
|
||||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
durationSec = it.lengthSeconds(),
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
albumArt = File(
|
||||||
try {
|
Environment.getExternalStorageDirectory(),
|
||||||
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
defaultDir + ".Images/" + it.videoId() + ".jpeg"
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
),
|
||||||
Log.i("YTDownloader", e.toString())
|
source = Source.YouTube,
|
||||||
null
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
|
downloaded = if (File(
|
||||||
|
finalOutputDir(
|
||||||
|
itemName = it.title(),
|
||||||
|
type = folderType,
|
||||||
|
subFolder = subFolder
|
||||||
|
)).exists()
|
||||||
|
)
|
||||||
|
DownloadStatus.Downloaded
|
||||||
|
else {
|
||||||
|
DownloadStatus.NotDownloaded
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
})
|
}.toMutableList())
|
||||||
|
|
||||||
|
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,
|
||||||
|
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
|
||||||
|
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){
|
||||||
|
showMessage("An Error Occurred While Processing!")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) {
|
||||||
|
if(!isOnline())return
|
||||||
|
try{
|
||||||
|
uiScope.launch(Dispatchers.IO) {
|
||||||
|
Log.i("YT Video",searchId)
|
||||||
|
val video = ytDownloader.getVideo(searchId)
|
||||||
|
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
|
||||||
|
val detail = video?.details()
|
||||||
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
|
||||||
|
Log.i("YT View Model",detail.toString())
|
||||||
|
this@YoutubeViewModel.trackList.postValue(
|
||||||
|
listOf(
|
||||||
|
TrackDetails(
|
||||||
|
title = name,
|
||||||
|
artists = listOf(detail?.author().toString()),
|
||||||
|
durationSec = detail?.lengthSeconds()?:0,
|
||||||
|
albumArt = File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
"$defaultDir.Images/$searchId.jpeg"
|
||||||
|
),
|
||||||
|
source = Source.YouTube,
|
||||||
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
|
)
|
||||||
|
).toMutableList()
|
||||||
|
)
|
||||||
|
title.postValue(
|
||||||
|
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
|
||||||
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
databaseDAO.insert(DownloadRecord(
|
databaseDAO.insert(DownloadRecord(
|
||||||
type = "Track",
|
type = "Track",
|
||||||
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
||||||
link = "https://www.youtube.com/watch?v=$searchId",
|
link = "https://www.youtube.com/watch?v=$searchId",
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg",
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
totalFiles = 1,
|
totalFiles = 1,
|
||||||
downloaded = false,
|
downloaded = false,
|
||||||
directory = finalOutputDir(type = "YT_Downloads")
|
directory = finalOutputDir(type = "YT_Downloads")
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e:com.github.kiulian.downloader.YoutubeException){
|
||||||
|
showMessage("An Error Occurred While Processing!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Shabinder Singh
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.spotiflyer.utils
|
|
||||||
|
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.view.animation.RotateAnimation
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
|
||||||
import com.bumptech.glide.request.RequestListener
|
|
||||||
import com.bumptech.glide.request.target.Target
|
|
||||||
import com.shabinder.spotiflyer.R
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{
|
|
||||||
return Environment.getExternalStorageDirectory().toString() + File.separator +
|
|
||||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator +
|
|
||||||
(if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator}
|
|
||||||
+ itemName?.let { SpotifyDownloadHelper.removeIllegalChars(it) + extension})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rotateAnim(view: View){
|
|
||||||
val rotate = RotateAnimation(
|
|
||||||
0F, 360F,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
|
||||||
)
|
|
||||||
rotate.duration = 1000
|
|
||||||
rotate.repeatCount = Animation.INFINITE
|
|
||||||
rotate.repeatMode = Animation.INFINITE
|
|
||||||
rotate.interpolator = LinearInterpolator()
|
|
||||||
view.animation = rotate
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@BindingAdapter("imageUrl")
|
|
||||||
fun bindImage(imgView: ImageView, imgUrl: String?) {
|
|
||||||
imgUrl?.let {
|
|
||||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
|
||||||
Glide
|
|
||||||
.with(imgView)
|
|
||||||
.asFile()
|
|
||||||
.load(imgUri)
|
|
||||||
.placeholder(R.drawable.ic_song_placeholder)
|
|
||||||
.error(R.drawable.ic_musicplaceholder)
|
|
||||||
.listener(object:RequestListener<File>{
|
|
||||||
override fun onLoadFailed(
|
|
||||||
e: GlideException?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
Log.i("Glide","LoadFailed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResourceReady(
|
|
||||||
resource: File?,
|
|
||||||
model: Any?,
|
|
||||||
target: Target<File>?,
|
|
||||||
dataSource: DataSource?,
|
|
||||||
isFirstResource: Boolean
|
|
||||||
): Boolean {
|
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
|
||||||
try {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
|
||||||
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
|
||||||
resource?.copyTo(file)
|
|
||||||
withContext(Dispatchers.Main){
|
|
||||||
Glide.with(imgView)
|
|
||||||
.load(file)
|
|
||||||
.placeholder(R.drawable.ic_song_placeholder)
|
|
||||||
.into(imgView)
|
|
||||||
// Log.i("Glide","imageSaved")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}).submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*Extension Function For Copying Files!
|
|
||||||
**/
|
|
||||||
fun File.copyTo(file: File) {
|
|
||||||
inputStream().use { input ->
|
|
||||||
file.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun createDirectory(dir:String){
|
|
||||||
val yourAppDir = File(Environment.getExternalStorageDirectory(),
|
|
||||||
dir)
|
|
||||||
|
|
||||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
|
||||||
{ // create empty directory
|
|
||||||
if (yourAppDir.mkdirs())
|
|
||||||
{Log.i("CreateDir","App dir created")}
|
|
||||||
else
|
|
||||||
{Log.w("CreateDir","Unable to create app dir!")}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{Log.i("CreateDir","App dir already exists")}
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.view.View
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
|
|
||||||
|
fun View.openPlatformOnClick(packageName:String, websiteAddress:String){
|
||||||
|
val manager: PackageManager = mainActivity.packageManager
|
||||||
|
try {
|
||||||
|
val i = manager.getLaunchIntentForPackage(packageName)
|
||||||
|
?: throw PackageManager.NameNotFoundException()
|
||||||
|
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
this.setOnClickListener { mainActivity.startActivity(i) }
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
val uri: Uri =
|
||||||
|
Uri.parse(websiteAddress)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
this.setOnClickListener { mainActivity.startActivity(intent) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun View.openPlatformOnClick(websiteAddress:String){
|
||||||
|
val uri: Uri =
|
||||||
|
Uri.parse(websiteAddress)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
|
this.setOnClickListener { mainActivity.startActivity(intent) }
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
|
||||||
|
const val NoInternetErrorCode = 222
|
||||||
|
|
||||||
|
class NetworkInterceptor: Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return if (!isOnline()){
|
||||||
|
//No Internet Connection
|
||||||
|
showNoConnectionAlert()
|
||||||
|
//Lets Stop the Incoming Request
|
||||||
|
Response.Builder()
|
||||||
|
.code(NoInternetErrorCode) // code(200.300) = successful else = unsuccessful
|
||||||
|
.body("{}".toResponseBody(null)) // Whatever body
|
||||||
|
.protocol(Protocol.HTTP_2)
|
||||||
|
.message("No Internet Connection")
|
||||||
|
.request(chain.request())
|
||||||
|
.build()
|
||||||
|
}else {
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
val responseBody = response.body
|
||||||
|
val bodyString = responseBody?.string()
|
||||||
|
//Log.i("Network Request",bodyString)
|
||||||
|
//chain.proceed(chain.request())
|
||||||
|
//Log.i("Network Request","{\"unchecked\":${bodyString}}")
|
||||||
|
Response.Builder()
|
||||||
|
.code(response.code) // code(200.300) = successful else = unsuccessful
|
||||||
|
.body("{\"value\":${bodyString}}".toResponseBody(responseBody?.contentType())) // Whatever body
|
||||||
|
.protocol(response.protocol)
|
||||||
|
.message(response.message)
|
||||||
|
.request(chain.request())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Converts REQUEST's Body to String
|
||||||
|
* */
|
||||||
|
private fun RequestBody?.bodyToString(): String {
|
||||||
|
if (this == null) return ""
|
||||||
|
val buffer = okio.Buffer()
|
||||||
|
writeTo(buffer)
|
||||||
|
return buffer.readUtf8()
|
||||||
|
}
|
||||||
|
}
|
@ -18,11 +18,15 @@
|
|||||||
package com.shabinder.spotiflyer.utils
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
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.MainActivity
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
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.SpotifyServiceTokenRequest
|
||||||
|
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||||
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
@ -36,23 +40,35 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
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 java.io.File
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
||||||
@InstallIn(ApplicationComponent::class)
|
@InstallIn(ApplicationComponent::class)
|
||||||
@Module
|
@Module
|
||||||
object Provider {
|
object Provider {
|
||||||
|
|
||||||
|
val mainActivity: MainActivity = MainActivity.getInstance()
|
||||||
|
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
||||||
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun getYTDownloader():YoutubeDownloader{
|
||||||
|
return YoutubeDownloader()
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUpi():EasyUpiPayment {
|
fun provideUpi():EasyUpiPayment {
|
||||||
return EasyUpiPayment.Builder(MainActivity.getInstance())
|
return EasyUpiPayment.Builder(mainActivity)
|
||||||
.setPayeeVpa("technoshab@paytm")
|
.setPayeeVpa("technoshab@paytm")
|
||||||
.setPayeeName("Shabinder Singh")
|
.setPayeeName("Shabinder Singh")
|
||||||
.setTransactionId("UNIQUE_TRANSACTION_ID")
|
.setTransactionId("UNIQUE_TRANSACTION_ID")
|
||||||
@ -72,29 +88,59 @@ object Provider {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun getYTDownloader():YoutubeDownloader{
|
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
|
||||||
return YoutubeDownloader()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
|
|
||||||
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
|
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||||
httpClient2.addInterceptor(Interceptor { chain ->
|
.addInterceptor(Interceptor { chain ->
|
||||||
val request: Request =
|
val request: Request =
|
||||||
chain.request().newBuilder().addHeader(
|
chain.request().newBuilder()
|
||||||
|
.addHeader(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
"Basic ${android.util.Base64.encodeToString("${App.clientId}:${App.clientSecret}".toByteArray(),android.util.Base64.NO_WRAP)}"
|
"Basic ${
|
||||||
|
android.util.Base64.encodeToString(
|
||||||
|
"${App.clientId}:${App.clientSecret}".toByteArray(),
|
||||||
|
android.util.Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
}"
|
||||||
).build()
|
).build()
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
})
|
}).addInterceptor(NetworkInterceptor())
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder()
|
val retrofit = Retrofit.Builder()
|
||||||
.baseUrl("https://accounts.spotify.com/")
|
.baseUrl("https://accounts.spotify.com/")
|
||||||
.client(httpClient2.build())
|
.client(httpClient2.build())
|
||||||
.addConverterFactory(MoshiConverterFactory.create(getMoshi()))
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
.build()
|
.build()
|
||||||
return retrofit.create(SpotifyServiceTokenRequest::class.java)
|
return retrofit.create(SpotifyServiceTokenRequest::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun okHttpClient():OkHttpClient{
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.addInterceptor(NetworkInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun getGaanaInterface(moshi: Moshi,okHttpClient: OkHttpClient):GaanaInterface{
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://api.gaana.com/")
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
|
.build()
|
||||||
|
return retrofit.create(GaanaInterface::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun getYoutubeMusicApi(moshi: Moshi): YoutubeMusicApi {
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl("https://music.youtube.com/youtubei/v1/")
|
||||||
|
.addConverterFactory(ScalarsConverterFactory.create())
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
|
.build()
|
||||||
|
return retrofit.create(YoutubeMusicApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.NavArgs
|
||||||
|
import com.shabinder.spotiflyer.R
|
||||||
|
import com.shabinder.spotiflyer.SharedViewModel
|
||||||
|
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
|
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
|
|
||||||
|
abstract class TrackListFragment<VM : TrackListViewModel , args: NavArgs> : Fragment() {
|
||||||
|
|
||||||
|
protected lateinit var sharedViewModel: SharedViewModel
|
||||||
|
protected lateinit var binding: TrackListFragmentBinding
|
||||||
|
protected abstract var viewModel: VM
|
||||||
|
protected abstract var adapter: TrackListAdapter
|
||||||
|
protected abstract var source: Source
|
||||||
|
private var intentFilter: IntentFilter? = null
|
||||||
|
private var updateUIReceiver: BroadcastReceiver? = null
|
||||||
|
protected abstract val args:NavArgs
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if(!isOnline()){
|
||||||
|
showNoConnectionAlert()
|
||||||
|
mainActivity.onBackPressed()
|
||||||
|
}
|
||||||
|
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
binding = TrackListFragmentBinding.inflate(inflater,container,false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
initializeLiveDataObservers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Live Data Observers
|
||||||
|
**/
|
||||||
|
private fun initializeLiveDataObservers() {
|
||||||
|
viewModel.trackList.observe(viewLifecycleOwner, {
|
||||||
|
if (!it.isNullOrEmpty()){
|
||||||
|
Log.i("GaanaFragment","TrackList Updated")
|
||||||
|
adapter.submitList(it, source)
|
||||||
|
checkIfAllDownloaded()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.coverUrl.observe(viewLifecycleOwner, {
|
||||||
|
it?.let{bindImage(binding.coverImage,it, source)}
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.title.observe(viewLifecycleOwner, {
|
||||||
|
binding.titleView.text = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeBroadcast() {
|
||||||
|
intentFilter = IntentFilter()
|
||||||
|
intentFilter?.addAction("track_download_completed")
|
||||||
|
|
||||||
|
updateUIReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
//UI update here
|
||||||
|
if (intent != null){
|
||||||
|
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
|
||||||
|
trackDetails?.let {
|
||||||
|
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
|
||||||
|
Log.i("Track","Download Completed Intent :$position")
|
||||||
|
if(position != -1) {
|
||||||
|
val track = viewModel.trackList.value?.get(position)
|
||||||
|
track?.let{
|
||||||
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
|
viewModel.trackList.value?.set(position, it)
|
||||||
|
adapter.notifyItemChanged(position)
|
||||||
|
checkIfAllDownloaded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
initializeBroadcast()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
requireActivity().unregisterReceiver(updateUIReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkIfAllDownloaded() {
|
||||||
|
if(!viewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
|
||||||
|
//All Tracks Downloaded
|
||||||
|
binding.btnDownloadAll.visibility = View.GONE
|
||||||
|
binding.downloadingFab.apply{
|
||||||
|
setImageResource(R.drawable.ic_tick)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
clearAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Shabinder Singh
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import kotlinx.coroutines.CompletableJob
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
|
||||||
|
abstract class TrackListViewModel:ViewModel() {
|
||||||
|
abstract var folderType:String
|
||||||
|
abstract var subFolder:String
|
||||||
|
open val trackList = MutableLiveData<MutableList<TrackDetails>>()
|
||||||
|
|
||||||
|
private val viewModelJob:CompletableJob = Job()
|
||||||
|
open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||||
|
|
||||||
|
private val loading = "Loading!"
|
||||||
|
open var title = MutableLiveData<String>().apply { value = loading }
|
||||||
|
open var coverUrl = MutableLiveData<String>()
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
viewModelJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
288
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
288
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
@ -17,16 +17,288 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.utils
|
package com.shabinder.spotiflyer.utils
|
||||||
|
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
|
import android.view.animation.RotateAnimation
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.shabinder.spotiflyer.R
|
||||||
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
|
import com.shabinder.spotiflyer.models.spotify.Source
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.defaultDir
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||||
|
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) {
|
||||||
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
|
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) }
|
||||||
|
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startService(context:Context?,objects:ArrayList<DownloadObject>? = null ) {
|
||||||
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
|
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) }
|
||||||
|
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{
|
||||||
|
return Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
defaultDir + removeIllegalChars(type) + File.separator +
|
||||||
|
(if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator}
|
||||||
|
+ itemName?.let { removeIllegalChars(it) + extension})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Util. Function To Check Connection Status
|
||||||
|
**/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun isOnline(): Boolean {
|
||||||
|
var result = false
|
||||||
|
val connectivityManager =
|
||||||
|
mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
|
||||||
|
connectivityManager?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
|
||||||
|
result = when {
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
|
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val netInfo =
|
||||||
|
(mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
|
||||||
|
result = netInfo != null && netInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false , isError:Boolean = false){
|
||||||
|
CoroutineScope(Dispatchers.Main).launch{
|
||||||
|
Snackbar.make(
|
||||||
|
mainActivity.snackBarAnchor,
|
||||||
|
message,
|
||||||
|
if (long) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
|
||||||
|
).apply {
|
||||||
|
setAction("Ok") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
setActionTextColor(ContextCompat.getColor(mainActivity,R.color.black))
|
||||||
|
when{
|
||||||
|
isSuccess -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.successGreen))
|
||||||
|
isError -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.errorRed))
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun rotateAnim(view: View){
|
||||||
|
val rotate = RotateAnimation(
|
||||||
|
0F, 360F,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
|
||||||
|
)
|
||||||
|
rotate.duration = 1000
|
||||||
|
rotate.repeatCount = Animation.INFINITE
|
||||||
|
rotate.repeatMode = Animation.INFINITE
|
||||||
|
rotate.interpolator = LinearInterpolator()
|
||||||
|
view.animation = rotate
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showNoConnectionAlert(){
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
mainActivity.apply {
|
||||||
|
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
|
||||||
|
.setTitle(resources.getString(R.string.title))
|
||||||
|
.setMessage(resources.getString(R.string.supporting_text))
|
||||||
|
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
|
||||||
|
// Respond to neutral button press
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
|
||||||
|
imgUrl?.let {
|
||||||
|
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
||||||
|
Glide
|
||||||
|
.with(imgView)
|
||||||
|
.asFile()
|
||||||
|
.load(imgUri)
|
||||||
|
.placeholder(R.drawable.ic_song_placeholder)
|
||||||
|
.error(R.drawable.ic_musicplaceholder)
|
||||||
|
.listener(object:RequestListener<File>{
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
Log.i("Glide","LoadFailed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: File?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
try {
|
||||||
|
val file = when(source){
|
||||||
|
Source.Spotify->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Source.YouTube->{
|
||||||
|
//Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
|
// We Are Naming using "$searchId"
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Source.Gaana -> {
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
Provider.defaultDir +".Images/" + (imgUrl.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
|
||||||
|
}
|
||||||
|
else -> File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
||||||
|
resource?.copyTo(file)
|
||||||
|
Glide.with(imgView)
|
||||||
|
.load(file)
|
||||||
|
.placeholder(R.drawable.ic_song_placeholder)
|
||||||
|
.into(imgView)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Extension Function For Copying Files!
|
||||||
|
**/
|
||||||
|
fun File.copyTo(file: File) {
|
||||||
|
inputStream().use { input ->
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun createDirectory(dir:String){
|
||||||
|
val yourAppDir = File(Environment.getExternalStorageDirectory(),
|
||||||
|
dir)
|
||||||
|
|
||||||
|
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||||
|
{ // create empty directory
|
||||||
|
if (yourAppDir.mkdirs())
|
||||||
|
{Log.i("CreateDir","App dir created")}
|
||||||
|
else
|
||||||
|
{Log.w("CreateDir","Unable to create app dir!")}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{Log.i("CreateDir","App dir already exists")}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Removing Illegal Chars from File Name
|
||||||
|
* **/
|
||||||
|
fun removeIllegalChars(fileName: String): String? {
|
||||||
|
val illegalCharArray = charArrayOf(
|
||||||
|
'/',
|
||||||
|
'\n',
|
||||||
|
'\r',
|
||||||
|
'\t',
|
||||||
|
'\u0000',
|
||||||
|
'\u000C',
|
||||||
|
'`',
|
||||||
|
'?',
|
||||||
|
'*',
|
||||||
|
'\\',
|
||||||
|
'<',
|
||||||
|
'>',
|
||||||
|
'|',
|
||||||
|
'\"',
|
||||||
|
'.',
|
||||||
|
'-',
|
||||||
|
'\''
|
||||||
|
)
|
||||||
|
|
||||||
|
var name = fileName
|
||||||
|
for (c in illegalCharArray) {
|
||||||
|
name = fileName.replace(c, '_')
|
||||||
|
}
|
||||||
|
name = name.replace("\\s".toRegex(), "_")
|
||||||
|
name = name.replace("\\)".toRegex(), "")
|
||||||
|
name = name.replace("\\(".toRegex(), "")
|
||||||
|
name = name.replace("\\[".toRegex(), "")
|
||||||
|
name = name.replace("]".toRegex(), "")
|
||||||
|
name = name.replace("\\.".toRegex(), "")
|
||||||
|
name = name.replace("\"".toRegex(), "")
|
||||||
|
name = name.replace("\'".toRegex(), "")
|
||||||
|
name = name.replace(":".toRegex(), "")
|
||||||
|
name = name.replace("\\|".toRegex(), "")
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
fun createDirectories() {
|
fun createDirectories() {
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir)
|
createDirectory(defaultDir)
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/")
|
createDirectory(defaultDir + ".Images/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/")
|
createDirectory(defaultDir + "Tracks/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/")
|
createDirectory(defaultDir + "Albums/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/")
|
createDirectory(defaultDir + "Playlists/")
|
||||||
createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/")
|
createDirectory(defaultDir + "YT_Downloads/")
|
||||||
}
|
}
|
||||||
fun getEmojiByUnicode(unicode: Int): String? {
|
fun getEmojiByUnicode(unicode: Int): String? {
|
||||||
return String(Character.toChars(unicode))
|
return String(Character.toChars(unicode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
internal val nullOnEmptyConverterFactory = object : Converter.Factory() {
|
||||||
|
fun converterFactory() = this
|
||||||
|
override fun responseBodyConverter(
|
||||||
|
type: Type,
|
||||||
|
annotations: Array<out Annotation>,
|
||||||
|
retrofit: Retrofit
|
||||||
|
) = object : Converter<ResponseBody, Any?> {
|
||||||
|
val nextResponseBodyConverter =
|
||||||
|
retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
|
||||||
|
|
||||||
|
override fun convert(value: ResponseBody) =
|
||||||
|
if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.shabinder.spotiflyer.worker
|
package com.shabinder.spotiflyer.worker
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
@ -28,27 +29,37 @@ import android.os.*
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.arthenica.mobileffmpeg.Config
|
import com.arthenica.mobileffmpeg.Config
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
||||||
import com.arthenica.mobileffmpeg.FFmpeg
|
import com.arthenica.mobileffmpeg.FFmpeg
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.DataSource
|
||||||
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
|
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||||
import com.mpatric.mp3agic.ID3v1Tag
|
import com.mpatric.mp3agic.ID3v1Tag
|
||||||
import com.mpatric.mp3agic.ID3v24Tag
|
import com.mpatric.mp3agic.ID3v24Tag
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
import com.shabinder.spotiflyer.MainActivity
|
import com.shabinder.spotiflyer.MainActivity
|
||||||
import com.shabinder.spotiflyer.R
|
import com.shabinder.spotiflyer.R
|
||||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
|
||||||
import com.shabinder.spotiflyer.models.DownloadObject
|
import com.shabinder.spotiflyer.models.DownloadObject
|
||||||
import com.shabinder.spotiflyer.models.Track
|
import com.shabinder.spotiflyer.models.TrackDetails
|
||||||
|
import com.shabinder.spotiflyer.utils.Provider
|
||||||
|
import com.shabinder.spotiflyer.utils.copyTo
|
||||||
import com.tonyodev.fetch2.*
|
import com.tonyodev.fetch2.*
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
class ForegroundService : Service(){
|
class ForegroundService : Service(){
|
||||||
private val tag = "Foreground Service"
|
private val tag = "Foreground Service"
|
||||||
private val channelId = "ForegroundDownloaderService"
|
private val channelId = "ForegroundDownloaderService"
|
||||||
@ -56,22 +67,21 @@ class ForegroundService : Service(){
|
|||||||
private var total = 0 //Total Downloads Requested
|
private var total = 0 //Total Downloads Requested
|
||||||
private var converted = 0//Total Files Converted
|
private var converted = 0//Total Files Converted
|
||||||
private var downloaded = 0//Total Files downloaded
|
private var downloaded = 0//Total Files downloaded
|
||||||
private var fetch:Fetch? = null
|
private lateinit var fetch:Fetch
|
||||||
private var downloadManager : DownloadManager? = null
|
private lateinit var ytDownloader: YoutubeDownloader
|
||||||
private var downloadList = mutableListOf<DownloadObject>()
|
private lateinit var downloadManager : DownloadManager
|
||||||
private var serviceJob = Job()
|
private var serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||||
private val requestMap = mutableMapOf<Request,Track>()
|
private val requestMap = mutableMapOf<Request, TrackDetails>()
|
||||||
private val downloadMap = mutableMapOf<String,Track>()
|
|
||||||
private var speed :Long = 0
|
private var speed :Long = 0
|
||||||
private var defaultDirectory = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
private var defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||||
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
|
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
|
||||||
defaultDirectory+File.separator
|
defaultDir +File.separator
|
||||||
)
|
)
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
var notificationLine = 0
|
var notificationLine = 0
|
||||||
val messageList = mutableListOf<String>("","","","")
|
val messageList = mutableListOf("","","","")
|
||||||
private var pendingIntent:PendingIntent? = null
|
private var pendingIntent:PendingIntent? = null
|
||||||
|
|
||||||
|
|
||||||
@ -88,7 +98,7 @@ class ForegroundService : Service(){
|
|||||||
0, notificationIntent, 0
|
0, notificationIntent, 0
|
||||||
)
|
)
|
||||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
ytDownloader = YoutubeDownloader()
|
||||||
val fetchConfiguration =
|
val fetchConfiguration =
|
||||||
FetchConfiguration.Builder(this)
|
FetchConfiguration.Builder(this)
|
||||||
.setDownloadConcurrentLimit(4)
|
.setDownloadConcurrentLimit(4)
|
||||||
@ -97,90 +107,41 @@ class ForegroundService : Service(){
|
|||||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||||
|
|
||||||
fetch = Fetch.getDefaultInstance()
|
fetch = Fetch.getDefaultInstance()
|
||||||
// fetch?.enableLogging(true)
|
fetch.addListener(fetchListener)
|
||||||
fetch?.addListener(fetchListener)
|
|
||||||
//clearing all not completed Downloads
|
//clearing all not completed Downloads
|
||||||
//Starting fresh
|
//Starting fresh
|
||||||
fetch?.removeAll()
|
fetch.removeAll()
|
||||||
|
|
||||||
startForeground()
|
startForeground()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@SuppressLint("WakelockTimeout")
|
||||||
*Starting Service with Notification as Foreground!
|
|
||||||
**/
|
|
||||||
private fun startForeground() {
|
|
||||||
val channelId =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel(channelId, "Downloader Service")
|
|
||||||
} else {
|
|
||||||
// If earlier version channel ID is not used
|
|
||||||
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, channelId)
|
|
||||||
.setSmallIcon(R.drawable.down_arrowbw)
|
|
||||||
.setNotificationSilent()
|
|
||||||
.setSubText("Total: $total Completed:$converted")
|
|
||||||
.setStyle(NotificationCompat.InboxStyle()
|
|
||||||
.setBigContentTitle("Speed: $speed KB/s")
|
|
||||||
.addLine(messageList[0])
|
|
||||||
.addLine(messageList[1])
|
|
||||||
.addLine(messageList[2])
|
|
||||||
.addLine(messageList[3]))
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
startForeground(notificationId, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
|
||||||
val chan = NotificationChannel(channelId,
|
|
||||||
channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
||||||
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
service.createNotificationChannel(chan)
|
|
||||||
return channelId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
// Send a notification that service is started
|
// Send a notification that service is started
|
||||||
Log.i(tag,"Service Started.")
|
Log.i(tag,"Service Started.")
|
||||||
startForeground()
|
startForeground()
|
||||||
//do heavy work on a background thread
|
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
|
||||||
//val list = intent.getSerializableExtra("list") as List<Any?>
|
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
|
||||||
// val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
|
||||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
|
||||||
val obj = intent.getParcelableExtra<DownloadObject>("object") ?: intent.extras?.getParcelable<DownloadObject>("object")
|
|
||||||
obj?.let {
|
|
||||||
total ++
|
|
||||||
// Log.i(tag,"Intent List Size: ${list!!.size}")
|
|
||||||
updateNotification()
|
|
||||||
serviceScope.launch {
|
|
||||||
val request= Request(obj.url, obj.outputDir)
|
|
||||||
request.priority = Priority.NORMAL
|
|
||||||
request.networkType = NetworkType.ALL
|
|
||||||
|
|
||||||
fetch!!.enqueue(request,
|
imagesList?.let{
|
||||||
{
|
serviceScope.launch {
|
||||||
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
loadAllImages(it)
|
||||||
downloadList.remove(obj)
|
|
||||||
Log.i(tag, "Enqueuing Download")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadObjects?.let {
|
||||||
|
total += downloadObjects.size
|
||||||
|
updateNotification()
|
||||||
|
downloadAllTracks(downloadObjects)
|
||||||
|
}
|
||||||
|
|
||||||
//Wake locks and misc tasks from here :
|
//Wake locks and misc tasks from here :
|
||||||
return if (isServiceStarted){
|
return if (isServiceStarted){
|
||||||
|
//Service Already Started
|
||||||
START_STICKY
|
START_STICKY
|
||||||
} else{
|
} else{
|
||||||
Log.i(tag,"Starting the foreground service task")
|
Log.i(tag,"Starting the foreground service task")
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
|
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
||||||
@ -191,9 +152,53 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){
|
||||||
|
serviceScope.launch(Dispatchers.IO) {
|
||||||
|
for(downloadObj in downloadObjects){
|
||||||
|
try {
|
||||||
|
val video = ytDownloader.getVideo(downloadObj.ytVideoId)
|
||||||
|
val format: Format? = try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||||
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
||||||
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
Log.i("YTDownloader", e.toString())
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format?.let {
|
||||||
|
val url: String = format.url()
|
||||||
|
Log.i("DHelper Link Found", url)
|
||||||
|
serviceScope.launch {
|
||||||
|
val request= Request(url, downloadObj.outputFile)
|
||||||
|
request.priority = Priority.NORMAL
|
||||||
|
request.networkType = NetworkType.ALL
|
||||||
|
|
||||||
|
fetch.enqueue(request,
|
||||||
|
{
|
||||||
|
requestMap[it] = downloadObj.trackDetails
|
||||||
|
Log.i(tag, "Enqueuing Download")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
||||||
|
Log.i("Service YT Error", e.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if(downloadMap.isEmpty() && converted == total){
|
if(converted == total){
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
Log.i(tag,"Service destroyed.")
|
Log.i(tag,"Service destroyed.")
|
||||||
deleteFile(parentDirectory)
|
deleteFile(parentDirectory)
|
||||||
@ -203,25 +208,11 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseWakeLock() {
|
|
||||||
Log.i(tag,"Releasing Wake Lock")
|
|
||||||
try {
|
|
||||||
wakeLock?.let {
|
|
||||||
if (it.isHeld) {
|
|
||||||
it.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.i(tag,"Service stopped without being started: ${e.message}")
|
|
||||||
}
|
|
||||||
isServiceStarted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
if(downloadMap.isEmpty() && converted == total ){
|
if(converted == total ){
|
||||||
Log.i(tag,"Service Removed.")
|
Log.i(tag,"Service Removed.")
|
||||||
|
deleteFile(parentDirectory)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
} else {
|
} else {
|
||||||
@ -230,25 +221,6 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Deleting All Residual Files except Mp3 Files
|
|
||||||
**/
|
|
||||||
private fun deleteFile(dir:File) {
|
|
||||||
Log.i(tag,"Starting Deletions in ${dir.path} ")
|
|
||||||
val fList = dir.listFiles()
|
|
||||||
fList?.let {
|
|
||||||
for (file in fList) {
|
|
||||||
if (file.isDirectory) {
|
|
||||||
deleteFile(file)
|
|
||||||
} else if(file.isFile) {
|
|
||||||
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
|
||||||
// Log.i(tag,"deleting ${file.path}")
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch Listener/ Responsible for Fetch Behaviour
|
* Fetch Listener/ Responsible for Fetch Behaviour
|
||||||
@ -277,23 +249,23 @@ class ForegroundService : Service(){
|
|||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
when(notificationLine){
|
when(notificationLine){
|
||||||
0 -> {
|
0 -> {
|
||||||
messageList[0] = "Downloading ${track?.name}"
|
messageList[0] = "Downloading ${track?.title}"
|
||||||
notificationLine = 1
|
notificationLine = 1
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
messageList[1] = "Downloading ${track?.name}"
|
messageList[1] = "Downloading ${track?.title}"
|
||||||
notificationLine = 2
|
notificationLine = 2
|
||||||
}
|
}
|
||||||
2-> {
|
2-> {
|
||||||
messageList[2] = "Downloading ${track?.name}"
|
messageList[2] = "Downloading ${track?.title}"
|
||||||
notificationLine = 3
|
notificationLine = 3
|
||||||
}
|
}
|
||||||
3 -> {
|
3 -> {
|
||||||
messageList[3] = "Downloading ${track?.name}"
|
messageList[3] = "Downloading ${track?.title}"
|
||||||
notificationLine = 0
|
notificationLine = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i(tag,"${track?.name} Download Started")
|
Log.i(tag,"${track?.title} Download Started")
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,32 +284,27 @@ class ForegroundService : Service(){
|
|||||||
override fun onCompleted(download: Download) {
|
override fun onCompleted(download: Download) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
for (message in messageList){
|
for (message in messageList){
|
||||||
if( message == "Downloading ${track?.name}"){
|
if( message == "Downloading ${track?.title}"){
|
||||||
|
//Remove Downloading Status from Notification
|
||||||
messageList[messageList.indexOf(message)] = ""
|
messageList[messageList.indexOf(message)] = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Notify Download Completed
|
|
||||||
val intent = Intent()
|
|
||||||
.setAction("track_download_completed")
|
|
||||||
.putExtra("track",track)
|
|
||||||
this@ForegroundService.sendBroadcast(intent)
|
|
||||||
|
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try{
|
try{
|
||||||
convertToMp3(download.file, track!!)
|
track?.let { convertToMp3(download.file, it) }
|
||||||
Log.i(tag,"${track.name} Download Completed")
|
Log.i(tag,"${track?.title} Download Completed")
|
||||||
}catch (e:KotlinNullPointerException
|
}catch (e:KotlinNullPointerException
|
||||||
){
|
){
|
||||||
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!")
|
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
|
||||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
downloaded++
|
downloaded++
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
speed = 0
|
speed = 0
|
||||||
updateNotification()
|
// updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleted(download: Download) {
|
override fun onDeleted(download: Download) {
|
||||||
@ -357,7 +324,7 @@ class ForegroundService : Service(){
|
|||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
downloaded++
|
downloaded++
|
||||||
Log.i(tag,download.error.throwable.toString())
|
Log.i(tag,download.error.throwable.toString())
|
||||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
}
|
}
|
||||||
@ -374,9 +341,9 @@ class ForegroundService : Service(){
|
|||||||
downloadedBytesPerSecond: Long
|
downloadedBytesPerSecond: Long
|
||||||
) {
|
) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec")
|
Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec")
|
||||||
speed = (downloadedBytesPerSecond/1000)
|
speed = (downloadedBytesPerSecond/1000)
|
||||||
updateNotification()
|
// updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -384,7 +351,7 @@ class ForegroundService : Service(){
|
|||||||
/**
|
/**
|
||||||
* If fetch Fails , Android Download Manager To RESCUE!!
|
* If fetch Fails , Android Download Manager To RESCUE!!
|
||||||
**/
|
**/
|
||||||
fun downloadUsingDM(url:String, outputDir:String, track: Track){
|
fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
val request = DownloadManager.Request(uri)
|
val request = DownloadManager.Request(uri)
|
||||||
.setAllowedNetworkTypes(
|
.setAllowedNetworkTypes(
|
||||||
@ -392,14 +359,14 @@ class ForegroundService : Service(){
|
|||||||
DownloadManager.Request.NETWORK_MOBILE
|
DownloadManager.Request.NETWORK_MOBILE
|
||||||
)
|
)
|
||||||
.setAllowedOverRoaming(false)
|
.setAllowedOverRoaming(false)
|
||||||
.setTitle(track.name)
|
.setTitle(track.title)
|
||||||
.setDescription("Spotify Downloader Working Up here...")
|
.setDescription("Spotify Downloader Working Up here...")
|
||||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
||||||
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
||||||
))
|
))
|
||||||
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
//Start Download
|
//Start Download
|
||||||
val downloadID = downloadManager?.enqueue(request)
|
val downloadID = downloadManager.enqueue(request)
|
||||||
Log.i("DownloadManager", "Download Request Sent")
|
Log.i("DownloadManager", "Download Request Sent")
|
||||||
|
|
||||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
@ -421,7 +388,7 @@ class ForegroundService : Service(){
|
|||||||
/**
|
/**
|
||||||
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
|
||||||
**/
|
**/
|
||||||
fun convertToMp3(filePath: String, track: Track){
|
fun convertToMp3(filePath: String, track: TrackDetails){
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
|
|
||||||
FFmpeg.executeAsync(
|
FFmpeg.executeAsync(
|
||||||
@ -444,7 +411,7 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeMp3Tags(filePath:String, track: Track){
|
private fun writeMp3Tags(filePath:String, track: TrackDetails){
|
||||||
var mp3File = Mp3File(filePath)
|
var mp3File = Mp3File(filePath)
|
||||||
mp3File = removeAllTags(mp3File)
|
mp3File = removeAllTags(mp3File)
|
||||||
mp3File = setId3v1Tags(mp3File,track)
|
mp3File = setId3v1Tags(mp3File,track)
|
||||||
@ -457,11 +424,17 @@ class ForegroundService : Service(){
|
|||||||
newFile.renameTo(file)
|
newFile.renameTo(file)
|
||||||
converted++
|
converted++
|
||||||
updateNotification()
|
updateNotification()
|
||||||
|
|
||||||
|
//Notify Download Completed
|
||||||
|
val intent = Intent()
|
||||||
|
.setAction("track_download_completed")
|
||||||
|
.putExtra("track",track)
|
||||||
|
this@ForegroundService.sendBroadcast(intent)
|
||||||
|
|
||||||
//All tasks completed (REST IN PEACE)
|
//All tasks completed (REST IN PEACE)
|
||||||
if(converted == total){
|
if(converted == total){
|
||||||
onDestroy()
|
onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -475,7 +448,7 @@ class ForegroundService : Service(){
|
|||||||
.setSubText("Total: $total Completed:$converted")
|
.setSubText("Total: $total Completed:$converted")
|
||||||
.setNotificationSilent()
|
.setNotificationSilent()
|
||||||
.setStyle(NotificationCompat.InboxStyle()
|
.setStyle(NotificationCompat.InboxStyle()
|
||||||
.setBigContentTitle("Speed: $speed KB/s")
|
// .setBigContentTitle("Speed: $speed KB/s")
|
||||||
.addLine(messageList[0])
|
.addLine(messageList[0])
|
||||||
.addLine(messageList[1])
|
.addLine(messageList[1])
|
||||||
.addLine(messageList[2])
|
.addLine(messageList[2])
|
||||||
@ -486,64 +459,42 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*Modifying Mp3 Tags with MetaData!
|
*Modifying Mp3 com.shabinder.spotiflyer.models.gaana.Tags with MetaData!
|
||||||
**/
|
**/
|
||||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
|
||||||
val id3v1Tag = ID3v1Tag()
|
val id3v1Tag = ID3v1Tag().apply {
|
||||||
id3v1Tag.track = track.disc_number.toString()
|
artist = track.artists.joinToString(",")
|
||||||
val artistsList = mutableListOf<String>()
|
title = track.title
|
||||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
album = track.albumName
|
||||||
id3v1Tag.artist = artistsList.joinToString()
|
year = track.year
|
||||||
id3v1Tag.title = track.name
|
comment = "Genres:${track.comment}"
|
||||||
id3v1Tag.album = track.album?.name
|
}
|
||||||
id3v1Tag.year = track.album?.release_date
|
|
||||||
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
|
||||||
mp3File.id3v1Tag = id3v1Tag
|
mp3File.id3v1Tag = id3v1Tag
|
||||||
return mp3File
|
return mp3File
|
||||||
}
|
}
|
||||||
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
|
private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File {
|
||||||
val id3v2Tag = ID3v24Tag()
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
id3v2Tag.track = track.disc_number.toString()
|
artist = track.artists.joinToString(",")
|
||||||
val artistsList = mutableListOf<String>()
|
title = track.title
|
||||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
album = track.albumName
|
||||||
id3v2Tag.artist = artistsList.joinToString()
|
year = track.year
|
||||||
id3v2Tag.title = track.name
|
comment = "Genres:${track.comment}"
|
||||||
id3v2Tag.album = track.album?.name
|
lyrics = "Gonna Implement Soon"
|
||||||
id3v2Tag.year = track.album?.release_date
|
url = track.trackUrl
|
||||||
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
}
|
||||||
id3v2Tag.lyrics = "Gonna Implement Soon"
|
val bytesArray = ByteArray(track.albumArt.length().toInt())
|
||||||
val copyrights = mutableListOf<String>()
|
try{
|
||||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
val fis = FileInputStream(track.albumArt)
|
||||||
id3v2Tag.copyright = copyrights.joinToString()
|
|
||||||
id3v2Tag.url = track.href
|
|
||||||
track.ytCoverUrl?.let {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir +".Images/" + it.substringAfterLast('/',it) + ".jpeg")
|
|
||||||
Log.i("Mp3Tags editing Tags",file.path)
|
|
||||||
//init array with file length
|
|
||||||
val bytesArray = ByteArray(file.length().toInt())
|
|
||||||
val fis = FileInputStream(file)
|
|
||||||
fis.read(bytesArray) //read file into bytes[]
|
fis.read(bytesArray) //read file into bytes[]
|
||||||
fis.close()
|
fis.close()
|
||||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
||||||
|
}catch (e:java.io.FileNotFoundException){
|
||||||
|
Log.i("Error","Couldn't Write Mp3 Album Art")
|
||||||
}
|
}
|
||||||
track.album?.let {
|
|
||||||
val file = File(
|
|
||||||
Environment.getExternalStorageDirectory(),
|
|
||||||
SpotifyDownloadHelper.defaultDir +".Images/" + (it.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
|
|
||||||
Log.i("Mp3Tags editing Tags",file.path)
|
|
||||||
//init array with file length
|
|
||||||
val bytesArray = ByteArray(file.length().toInt())
|
|
||||||
val fis = FileInputStream(file)
|
|
||||||
fis.read(bytesArray) //read file into bytes[]
|
|
||||||
fis.close()
|
|
||||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
|
||||||
}
|
|
||||||
id3v2Tag.albumImage
|
|
||||||
mp3file.id3v2Tag = id3v2Tag
|
mp3file.id3v2Tag = id3v2Tag
|
||||||
return mp3file
|
return mp3file
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
||||||
if (mp3file.hasId3v1Tag()) {
|
if (mp3file.hasId3v1Tag()) {
|
||||||
mp3file.removeId3v1Tag()
|
mp3file.removeId3v1Tag()
|
||||||
@ -557,4 +508,146 @@ class ForegroundService : Service(){
|
|||||||
return mp3file
|
return mp3file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun releaseWakeLock() {
|
||||||
|
Log.i(tag,"Releasing Wake Lock")
|
||||||
|
try {
|
||||||
|
wakeLock?.let {
|
||||||
|
if (it.isHeld) {
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i(tag,"Service stopped without being started: ${e.message}")
|
||||||
|
}
|
||||||
|
isServiceStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*Starting Service with Notification as Foreground!
|
||||||
|
**/
|
||||||
|
private fun startForeground() {
|
||||||
|
val channelId =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel(channelId, "Downloader Service")
|
||||||
|
} else {
|
||||||
|
// If earlier version channel ID is not used
|
||||||
|
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(R.drawable.down_arrowbw)
|
||||||
|
.setNotificationSilent()
|
||||||
|
.setSubText("Total: $total Completed:$converted")
|
||||||
|
.setStyle(NotificationCompat.InboxStyle()
|
||||||
|
// .setBigContentTitle("Speed: $speed KB/s")
|
||||||
|
.addLine(messageList[0])
|
||||||
|
.addLine(messageList[1])
|
||||||
|
.addLine(messageList[2])
|
||||||
|
.addLine(messageList[3]))
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createNotificationChannel(channelId: String, channelName: String): String{
|
||||||
|
val chan = NotificationChannel(channelId,
|
||||||
|
channelName, NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
|
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
service.createNotificationChannel(chan)
|
||||||
|
return channelId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deleting All Residual Files except Mp3 Files
|
||||||
|
**/
|
||||||
|
private fun deleteFile(dir:File) {
|
||||||
|
Log.i(tag,"Starting Deletions in ${dir.path} ")
|
||||||
|
val fList = dir.listFiles()
|
||||||
|
fList?.let {
|
||||||
|
for (file in fList) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
deleteFile(file)
|
||||||
|
} else if(file.isFile) {
|
||||||
|
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
||||||
|
Log.i(tag,"deleting ${file.path}")
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to fetch all Images for use in mp3 tags.
|
||||||
|
**/
|
||||||
|
private suspend fun loadAllImages(urlList: ArrayList<String>) {
|
||||||
|
/*
|
||||||
|
* Last Element of this List defines Its Source
|
||||||
|
* */
|
||||||
|
val source = urlList.last()
|
||||||
|
for (url in urlList.subList(0,urlList.size-2)) {
|
||||||
|
val imgUri = url.toUri().buildUpon().scheme("https").build()
|
||||||
|
Glide
|
||||||
|
.with(this)
|
||||||
|
.asFile()
|
||||||
|
.load(imgUri)
|
||||||
|
.listener(object: RequestListener<File> {
|
||||||
|
override fun onLoadFailed(
|
||||||
|
e: GlideException?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
Log.i("Glide","LoadFailed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(
|
||||||
|
resource: File?,
|
||||||
|
model: Any?,
|
||||||
|
target: Target<File>?,
|
||||||
|
dataSource: DataSource?,
|
||||||
|
isFirstResource: Boolean
|
||||||
|
): Boolean {
|
||||||
|
serviceScope.launch {
|
||||||
|
withContext(Dispatchers.IO){
|
||||||
|
try {
|
||||||
|
val file = when(source){
|
||||||
|
"spotify" ->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"youtube" ->{
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"gaana" -> {
|
||||||
|
File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
Provider.defaultDir +".Images/" + (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
|
||||||
|
}
|
||||||
|
else -> File(
|
||||||
|
Environment.getExternalStorageDirectory(),
|
||||||
|
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg")
|
||||||
|
}
|
||||||
|
resource?.copyTo(file)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}).submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
BIN
app/src/main/res/drawable/gaana.png
Normal file
BIN
app/src/main/res/drawable/gaana.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -16,7 +16,8 @@
|
|||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||||
|
<solid android:color="@color/black"/>
|
||||||
<gradient
|
<gradient
|
||||||
android:angle="90"
|
android:angle="90"
|
||||||
android:centerColor="#0F6200FF"
|
android:centerColor="#0F6200FF"
|
||||||
|
27
app/src/main/res/drawable/ic_gaana.xml
Normal file
27
app/src/main/res/drawable/ic_gaana.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (C) 2020 Shabinder Singh
|
||||||
|
~
|
||||||
|
~ This program is free software: you can redistribute it and/or modify
|
||||||
|
~ it under the terms of the GNU General Public License as published by
|
||||||
|
~ the Free Software Foundation, either version 3 of the License, or
|
||||||
|
~ (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ This program is distributed in the hope that it will be useful,
|
||||||
|
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
~ GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="83dp"
|
||||||
|
android:height="45dp"
|
||||||
|
android:viewportWidth="83"
|
||||||
|
android:viewportHeight="33">
|
||||||
|
<path
|
||||||
|
android:fillColor="#F42C30"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M58.7,19.8c0.2,-1 0.3,-2 0.5,-2.9c0.6,-3.2 1.1,-6.4 1.7,-9.6c0.2,-0.9 -0.4,-1.6 -1.4,-1.6c-0.6,0 -1.2,0 -1.9,0c-1.3,0 -2.1,0.8 -2.4,2c-0.7,3.9 -1.4,7.9 -2.1,11.8c0,0.1 -0.2,0.3 -0.3,0.3c-1.8,0 -3.5,0 -5.3,-0.2c-1.1,-0.1 -2.1,-0.6 -2.7,-1.6c0,0 -0.1,-0.1 -0.1,-0.2c-0.2,0.3 -0.5,0.5 -0.7,0.7c-0.8,0.8 -1.7,1.3 -2.9,1.2c-0.8,-0.1 -1.7,0 -2.5,-0.1c-1.4,-0.3 -2.6,-1 -3.3,-2.4c-0.1,0.5 -0.2,1 -0.3,1.4c-0.2,1.2 -0.2,1.1 -1.4,1.1c-0.9,0 -1.7,-0.2 -2.4,-0.6c-0.6,-0.3 -1,-0.8 -1.6,-1.3c-0.2,0.2 -0.4,0.4 -0.6,0.7c-0.8,0.8 -1.7,1.2 -2.9,1.2c-1.1,0 -2.2,0 -3.3,-0.4c-1.9,-0.7 -3,-2.4 -2.6,-4.4c0.4,-2.6 0.8,-5.3 1.4,-7.9c0.6,-2.6 2.2,-4.4 5,-5c0.4,-0.1 0.8,-0.1 1.2,-0.1c2.3,0 4.6,0 6.9,0c0.1,0 0.2,0 0.4,0c-0.2,1.3 -0.5,2.5 -0.7,3.8c-0.5,2.9 -1,5.7 -1.5,8.6c0,0.3 0.1,0.6 0.2,0.8c0.3,0.6 1.4,1 2.1,0.9c0.3,-3 0.9,-6 1.5,-9c0.6,-2.6 2.2,-4.5 5,-5c0.4,-0.1 0.8,-0.1 1.2,-0.1c2.3,0 4.6,0 6.9,0c0.1,0 0.2,0 0.4,0c-0.1,0.8 -0.3,1.5 -0.4,2.3c-0.6,3.4 -1.2,6.8 -1.9,10.2c-0.1,0.5 0.1,0.9 0.5,1.2c0.2,0.1 0.3,0.2 0.5,0.3c0.4,0.1 0.8,0.3 1.1,0.2c0.5,-0.1 0.3,-0.6 0.4,-1c0.7,-3.8 1.4,-7.7 2.1,-11.5c0.1,-0.5 0.2,-1.1 0.3,-1.6c0.1,0 0.3,0 0.4,0c2.3,0 4.5,0 6.8,0c0.9,0 1.7,0.2 2.5,0.6c1.4,0.7 2.1,1.7 2.1,3.2c0,1.3 -0.3,2.5 -0.5,3.7c-0.5,3.1 -1.1,6.3 -1.7,9.4c0,0.2 0,0.4 -0.1,0.5c0,0.1 -0.2,0.2 -0.2,0.2C61,19.8 59.9,19.8 58.7,19.8L58.7,19.8zM30.8,5.7c-0.1,0 -0.2,0 -0.2,0c-1.1,0 -2.3,0 -3.4,0c-1.1,0 -2,0.7 -2.2,1.8c-0.4,2.3 -0.9,4.7 -1.3,7c-0.2,1.2 0.4,1.9 1.7,1.9c0.5,0 1,0 1.5,0c1.4,0 2.2,-0.7 2.4,-2c0.4,-2.1 0.8,-4.1 1.1,-6.2C30.5,7.3 30.7,6.5 30.8,5.7L30.8,5.7zM46,5.7c-0.1,0 -0.2,0 -0.2,0c-1.2,0 -2.3,0 -3.4,0c-1.2,0 -2,0.7 -2.2,1.8c-0.4,2.3 -0.9,4.7 -1.3,7c-0.2,1.1 0.3,1.7 1.4,1.8c0.6,0.1 1.2,0 1.8,0c1.3,0 2.2,-0.7 2.4,-2l1.1,-5.7C45.6,7.7 45.8,6.7 46,5.7zM14.6,19.8c-0.3,0 -0.6,0 -0.8,0c-1.3,0 -2.5,0 -3.8,-0.1c-1.9,-0.2 -3.2,-1.4 -3.6,-3c-0.2,-0.9 -0.1,-1.7 0.1,-2.6c0.4,-2.3 0.8,-4.6 1.3,-6.9c0.5,-2.8 2.7,-4.8 5.6,-5c1.7,-0.1 3.4,-0.1 5.1,-0.1c0.9,0 1.8,0 2.8,0c-0.1,0.7 -0.2,1.4 -0.3,2c-0.7,3.7 -1.3,7.3 -2,11c-0.4,2.3 -0.8,4.6 -1.3,6.9c-0.6,3.1 -3.1,4.6 -5.5,4.9C11.6,27 11,27 10.4,27c-2.1,0 -4.2,0 -6.3,0c0,0 -0.1,0 -0.1,0c0,-0.1 0,-0.1 0,-0.2c0.6,-1 1.2,-2.1 1.8,-3.1c0.1,-0.1 0.4,-0.2 0.6,-0.2c1.8,0 3.7,0 5.5,0c1.4,0 2.1,-0.6 2.4,-1.9C14.4,21 14.5,20.5 14.6,19.8L14.6,19.8zM13.2,16.3L13.2,16.3c0.5,0.1 0.9,0 1.4,0.1c0.5,0 0.6,-0.1 0.7,-0.6c0.5,-2.8 1,-5.6 1.5,-8.4c0.2,-1.1 -0.4,-1.7 -1.5,-1.8c-0.6,0 -1.2,0 -1.8,0c-1.2,0 -2,0.7 -2.3,1.9c-0.2,1.3 -0.5,2.6 -0.7,3.8c-0.2,1.2 -0.4,2.3 -0.6,3.5c-0.1,0.8 0.4,1.4 1.2,1.5C11.9,16.4 12.6,16.3 13.2,16.3zM78.9,2.1c-0.3,1.8 -0.6,3.5 -0.9,5.2c-0.4,2.4 -0.9,4.8 -1.3,7.1c-0.2,1 0.2,1.6 1.2,1.9c0.1,0 0.3,0.3 0.2,0.4c-0.2,1 -0.4,2 -0.5,3.1c-1.6,-0.1 -3,-0.6 -4,-1.9c-0.3,0.3 -0.5,0.5 -0.7,0.8c-0.8,0.8 -1.7,1.2 -2.9,1.2c-1,-0.1 -2,0 -3,-0.3c-2.2,-0.7 -3.3,-2.4 -3,-4.7c0.4,-2.6 0.8,-5.2 1.4,-7.8c0.6,-2.4 2,-4.2 4.6,-4.8c0.5,-0.1 1.1,-0.2 1.6,-0.2c2.4,0 4.7,0 7.1,0C78.7,2.1 78.8,2.1 78.9,2.1L78.9,2.1zM74.7,5.7c-0.2,0 -0.3,0 -0.4,0c-1.1,0 -2.2,0 -3.3,0c-1.1,0 -2,0.7 -2.2,1.8c-0.5,2.4 -0.9,4.8 -1.3,7.3c-0.2,0.9 0.3,1.5 1.3,1.7c0.6,0.1 1.2,0.1 1.9,0.1c1.4,0 2.2,-0.7 2.5,-2.1c0.4,-2.3 0.8,-4.5 1.2,-6.7C74.5,6.9 74.6,6.3 74.7,5.7z"/>
|
||||||
|
</vector>
|
BIN
app/src/main/res/font/nunito_sans.ttf
Normal file
BIN
app/src/main/res/font/nunito_sans.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/nunito_sans_light.ttf
Normal file
BIN
app/src/main/res/font/nunito_sans_light.ttf
Normal file
Binary file not shown.
@ -15,10 +15,9 @@
|
|||||||
~ You should have received a copy of the GNU General Public License
|
~ You should have received a copy of the GNU General Public License
|
||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
@ -27,14 +26,10 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:background="@drawable/text_background_accented"
|
style="@style/Widget.AppCompat.TextView.Gradient"
|
||||||
android:drawablePadding="5dp"
|
android:drawablePadding="5dp"
|
||||||
android:fontFamily="@font/raleway_semibold"
|
android:fontFamily="@font/raleway_semibold"
|
||||||
android:gravity="center"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:text=" Download History "
|
android:text=" Download History "
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="#E1FFFFFF"
|
|
||||||
android:textSize="21sp"
|
android:textSize="21sp"
|
||||||
app:drawableStartCompat="@drawable/ic_history"
|
app:drawableStartCompat="@drawable/ic_history"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@ -43,6 +38,7 @@
|
|||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tabLayout"
|
android:id="@+id/tabLayout"
|
||||||
|
android:background="@color/black"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
@ -60,6 +56,11 @@
|
|||||||
android:icon="@drawable/ic_spotify_logo"
|
android:icon="@drawable/ic_spotify_logo"
|
||||||
android:text="Spotify" />
|
android:text="Spotify" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabItem
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:icon="@drawable/gaana"
|
||||||
|
android:text="Gaana" />
|
||||||
<com.google.android.material.tabs.TabItem
|
<com.google.android.material.tabs.TabItem
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -81,5 +82,4 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
|
@ -17,89 +17,81 @@
|
|||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<data>
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
<variable
|
android:layout_width="match_parent"
|
||||||
name="downloadRecord"
|
android:layout_height="92dp"
|
||||||
type="com.shabinder.spotiflyer.database.DownloadRecord" />
|
android:background="#000000"
|
||||||
</data>
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
<RelativeLayout
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/coverUrl"
|
||||||
android:layout_height="92dp"
|
android:layout_width="100dp"
|
||||||
android:background="#000000"
|
android:layout_height="80dp"
|
||||||
android:paddingBottom="12dp">
|
android:layout_alignParentStart="true"
|
||||||
|
android:contentDescription="Track Image"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@drawable/ic_song_placeholder" />
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/coverUrl"
|
android:id="@+id/item_name"
|
||||||
android:layout_width="100dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentTop="true"
|
||||||
android:contentDescription="Track Image"
|
android:layout_marginStart="12dp"
|
||||||
android:scaleType="centerInside"
|
android:layout_marginTop="14dp"
|
||||||
android:src="@drawable/ic_song_placeholder" />
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_toStartOf="@+id/btn_action"
|
||||||
|
android:layout_toEndOf="@+id/coverUrl"
|
||||||
|
android:fontFamily="@font/raleway_semibold"
|
||||||
|
android:letterSpacing="0.04"
|
||||||
|
android:lines="1"
|
||||||
|
android:text="Weekend Chills"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
|
||||||
|
android:textColor="#9AB3FF"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/item_name"
|
android:id="@+id/type"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignBottom="@+id/coverUrl"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:layout_marginTop="14dp"
|
android:layout_marginEnd="0dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginBottom="15dp"
|
||||||
android:layout_toStartOf="@+id/btn_action"
|
android:layout_toStartOf="@+id/totalItems"
|
||||||
android:layout_toEndOf="@+id/coverUrl"
|
android:layout_toEndOf="@+id/coverUrl"
|
||||||
android:fontFamily="@font/raleway_semibold"
|
android:paddingLeft="9dp"
|
||||||
android:letterSpacing="0.04"
|
android:text="Playlist"
|
||||||
android:lines="1"
|
android:textSize="12sp" />
|
||||||
android:text="Weekend Chills"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
|
|
||||||
android:textColor="#9AB3FF"
|
|
||||||
android:textSize="20sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/type"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignBottom="@+id/coverUrl"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginEnd="0dp"
|
|
||||||
android:layout_marginBottom="15dp"
|
|
||||||
android:layout_toStartOf="@+id/totalItems"
|
|
||||||
android:layout_toEndOf="@+id/coverUrl"
|
|
||||||
android:paddingLeft="9dp"
|
|
||||||
android:text="Playlist"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/totalItems"
|
android:id="@+id/totalItems"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignBottom="@+id/coverUrl"
|
android:layout_alignBottom="@+id/coverUrl"
|
||||||
android:layout_marginTop="7dp"
|
android:layout_marginTop="7dp"
|
||||||
android:layout_marginEnd="25dp"
|
android:layout_marginEnd="25dp"
|
||||||
android:layout_marginBottom="15dp"
|
android:layout_marginBottom="15dp"
|
||||||
android:layout_toStartOf="@+id/btn_action"
|
android:layout_toStartOf="@+id/btn_action"
|
||||||
android:paddingLeft="9dp"
|
android:paddingLeft="9dp"
|
||||||
android:text="50 Tracks"
|
android:text="50 Tracks"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_action"
|
android:id="@+id/btn_action"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:layout_alignBottom="@+id/coverUrl"
|
android:layout_alignBottom="@+id/coverUrl"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp"
|
||||||
android:backgroundTint="@color/black"
|
android:backgroundTint="@color/black"
|
||||||
android:scaleType="centerInside"
|
android:scaleType="centerInside"
|
||||||
android:src="@drawable/ic_share_open"
|
android:src="@drawable/ic_share_open"
|
||||||
android:tint="@null" />
|
android:tint="@null" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
</layout>
|
|
||||||
|
|
||||||
|
@ -16,43 +16,35 @@
|
|||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<View
|
||||||
|
android:id="@+id/snackBarPosition"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="@drawable/transparent"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<fragment
|
||||||
android:id="@+id/mainActivity"
|
android:id="@+id/navHostFragment"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="0dp"
|
||||||
<TextView
|
app:defaultNavHost="true"
|
||||||
android:id="@+id/message"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_width="wrap_content"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:layout_height="wrap_content"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_marginBottom="2dp"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:background="@drawable/text_background_accented"
|
app:navGraph="@navigation/navigation"
|
||||||
android:padding="5dp"
|
tools:ignore="FragmentTagUsage" />
|
||||||
android:visibility="gone"
|
|
||||||
android:paddingTop="6dp"
|
|
||||||
android:text="Authentication Needed"
|
|
||||||
android:textColor="@color/colorPrimary"
|
|
||||||
android:textSize="10dp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<fragment
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/NavHostFragment"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/message"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navGraph="@navigation/navigation"
|
|
||||||
tools:ignore="FragmentTagUsage" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</layout>
|
|
@ -15,46 +15,40 @@
|
|||||||
~ You should have received a copy of the GNU General Public License
|
~ You should have received a copy of the GNU General Public License
|
||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fillViewport="true"
|
android:fillViewport="true"
|
||||||
|
tools:ignore="HardcodedText"
|
||||||
>
|
>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/linkSearch"
|
android:id="@+id/linkSearch"
|
||||||
android:layout_width="wrap_content"
|
style="@style/Widget.AppCompat.TextView.Gradient"
|
||||||
android:layout_height="46dp"
|
android:layout_height="46dp"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:background="@drawable/text_background_accented"
|
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:hint="Paste Link here"
|
android:hint="Paste Link here"
|
||||||
android:inputType="text"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textColorHint="@color/grey"
|
|
||||||
android:textSize="19sp"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_search"
|
app:layout_constraintEnd_toStartOf="@+id/btn_search"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread"
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:importantForAutofill="no"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btn_search"
|
android:id="@+id/btn_search"
|
||||||
android:layout_width="wrap_content"
|
style="@style/Widget.AppCompat.Button.Colored.Gradient"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:background="@drawable/btn_design"
|
android:fontFamily="@font/nunito_sans_light"
|
||||||
android:paddingLeft="4dp"
|
android:text=" Search "
|
||||||
android:paddingRight="4dp"
|
android:textStyle="bold"
|
||||||
android:text="Search"
|
|
||||||
android:textColor="@color/black"
|
|
||||||
android:textSize="16sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/linkSearch"
|
app:layout_constraintBottom_toBottomOf="@+id/linkSearch"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/linkSearch"
|
app:layout_constraintStart_toEndOf="@+id/linkSearch"
|
||||||
@ -74,19 +68,16 @@
|
|||||||
app:layout_collapseMode="parallax"
|
app:layout_collapseMode="parallax"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/linkSearch" />
|
app:layout_constraintTop_toBottomOf="@+id/linkSearch"
|
||||||
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_history"
|
android:id="@+id/btn_history"
|
||||||
android:layout_width="40dp"
|
style="@style/Widget.AppCompat.ImageButton.40dp"
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="@drawable/transparent"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/appLogo"
|
app:layout_constraintBottom_toBottomOf="@+id/appLogo"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:srcCompat="@drawable/ic_history" />
|
android:contentDescription="Open Download History Button"
|
||||||
|
app:srcCompat="@drawable/ic_history"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/appName"
|
android:id="@+id/appName"
|
||||||
@ -94,16 +85,13 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:fontFamily="@font/raleway_semibold"
|
android:fontFamily="@font/raleway_semibold"
|
||||||
android:gravity="end"
|
|
||||||
android:text='"SpotiFlyer"'
|
android:text='"SpotiFlyer"'
|
||||||
android:textAlignment="viewEnd"
|
android:textColor="@color/colorAccent"
|
||||||
android:textColor="#9AB3FF"
|
|
||||||
android:textSize="40sp"
|
android:textSize="40sp"
|
||||||
android:typeface="normal"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/appLogo" />
|
app:layout_constraintTop_toBottomOf="@id/appLogo"
|
||||||
|
/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/appSubTitle"
|
android:id="@+id/appSubTitle"
|
||||||
@ -119,49 +107,36 @@
|
|||||||
app:layout_constraintStart_toStartOf="@+id/appName"
|
app:layout_constraintStart_toStartOf="@+id/appName"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appName" />
|
app:layout_constraintTop_toBottomOf="@+id/appName" />
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/platforms"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:layout_marginEnd="2dp"
|
|
||||||
android:fontFamily="@font/raleway_semibold"
|
|
||||||
android:text="Supports: "
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="#9AB3FF"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_spotify"
|
|
||||||
app:layout_constraintHorizontal_chainStyle="packed"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/appSubTitle" />
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_spotify"
|
android:id="@+id/btn_spotify"
|
||||||
android:layout_width="46dp"
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
android:layout_height="46dp"
|
|
||||||
android:layout_marginEnd="2dp"
|
android:layout_marginEnd="2dp"
|
||||||
android:background="@color/black"
|
android:contentDescription="Open Spotify App Button"
|
||||||
android:padding="5dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_spotify_logo"
|
android:src="@drawable/ic_spotify_logo"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/platforms"
|
app:layout_constraintEnd_toStartOf="@+id/btn_Gaana"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_youtube"
|
android:padding="6dp"
|
||||||
app:layout_constraintStart_toEndOf="@+id/platforms"
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
app:layout_constraintTop_toTopOf="@+id/platforms" />
|
app:layout_constraintStart_toStartOf="@+id/appSubTitle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/appSubTitle"/>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_youtube"
|
android:id="@+id/btn_youtube"
|
||||||
android:layout_width="52dp"
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
android:layout_height="52dp"
|
android:contentDescription="Open Youtube App Button"
|
||||||
android:background="@color/black"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_youtube"
|
android:src="@drawable/ic_youtube"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/btn_spotify"
|
app:layout_constraintBottom_toBottomOf="@+id/btn_spotify"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="@+id/appSubTitle"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/btn_Gaana"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/btn_spotify"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_Gaana"
|
||||||
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
|
android:contentDescription="Open Gaana App Button"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:src="@drawable/gaana"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/btn_spotify"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn_youtube"
|
||||||
app:layout_constraintStart_toEndOf="@+id/btn_spotify"
|
app:layout_constraintStart_toEndOf="@+id/btn_spotify"
|
||||||
app:layout_constraintTop_toTopOf="@+id/btn_spotify" />
|
app:layout_constraintTop_toTopOf="@+id/btn_spotify" />
|
||||||
|
|
||||||
@ -170,15 +145,15 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center"
|
||||||
android:text="Usage Instructions!"
|
android:text="Usage Instructions!"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:textColor="#D0838383"
|
android:textColor="#D0838383"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:gravity="center"
|
app:layout_constraintBottom_toBottomOf="@+id/btn_Insta"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/developer_insta_spotify"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/btn_linkedin"
|
app:layout_constraintStart_toEndOf="@+id/btn_linkedin"
|
||||||
app:layout_constraintTop_toTopOf="@+id/btn_github_spotify" />
|
app:layout_constraintTop_toTopOf="@+id/btn_github" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btn_donate"
|
android:id="@+id/btn_donate"
|
||||||
@ -188,6 +163,7 @@
|
|||||||
android:background="@drawable/text_background_accented"
|
android:background="@drawable/text_background_accented"
|
||||||
android:drawableEnd="@drawable/ic_mug"
|
android:drawableEnd="@drawable/ic_mug"
|
||||||
android:drawablePadding="5dp"
|
android:drawablePadding="5dp"
|
||||||
|
android:contentDescription="Donate Money Button"
|
||||||
android:fontFamily="@font/capriola"
|
android:fontFamily="@font/capriola"
|
||||||
android:foreground="@drawable/rounded_gradient"
|
android:foreground="@drawable/rounded_gradient"
|
||||||
android:gravity="end|center_vertical"
|
android:gravity="end|center_vertical"
|
||||||
@ -212,40 +188,31 @@
|
|||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_github_spotify"
|
android:id="@+id/btn_github"
|
||||||
android:layout_width="48dp"
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/black"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_github"
|
android:src="@drawable/ic_github"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/btn_linkedin"
|
app:layout_constraintBottom_toTopOf="@+id/btn_linkedin"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/btn_youtube"
|
app:layout_constraintTop_toBottomOf="@+id/btn_youtube"
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
android:contentDescription="Open Github App Button"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"/>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_linkedin"
|
android:id="@+id/btn_linkedin"
|
||||||
android:layout_width="48dp"
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/black"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_linkedin"
|
android:src="@drawable/ic_linkedin"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/developer_insta_spotify"
|
app:layout_constraintBottom_toTopOf="@+id/btn_Insta"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/btn_github_spotify" />
|
android:contentDescription="Open LinkedIN App Button"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/btn_github"/>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/developer_insta_spotify"
|
android:id="@+id/btn_Insta"
|
||||||
android:layout_width="48dp"
|
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@color/black"
|
|
||||||
android:padding="5dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="@drawable/ic_instagram"
|
android:src="@drawable/ic_instagram"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/btn_donate"
|
app:layout_constraintBottom_toTopOf="@+id/btn_donate"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:contentDescription="Open Instagram App Button"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/btn_linkedin" />
|
app:layout_constraintTop_toBottomOf="@+id/btn_linkedin" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -288,5 +255,4 @@
|
|||||||
app:layout_constraintStart_toEndOf="@+id/heart" />
|
app:layout_constraintStart_toEndOf="@+id/heart" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</layout>
|
|
@ -16,19 +16,20 @@
|
|||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/main_youtube"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="25dp"
|
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
|
android:paddingTop="16dp"
|
||||||
tools:context=".ui.spotify.SpotifyFragment">
|
tools:context=".ui.spotify.SpotifyFragment">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/btn_download_all_spotify"
|
android:id="@+id/btn_download_all"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="44dp"
|
android:layout_height="44dp"
|
||||||
android:background="@drawable/btn_design"
|
android:background="@drawable/btn_design"
|
||||||
@ -39,19 +40,18 @@
|
|||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_anchor="@+id/appbar_spotify"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center" />
|
app:layout_anchorGravity="bottom|center" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/downloading_fab_spotify"
|
android:id="@+id/downloading_fab"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:keepScreenOn="true"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:backgroundTint="@color/black"
|
android:backgroundTint="@color/black"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:borderWidth="0dp"
|
app:borderWidth="0dp"
|
||||||
app:layout_anchor="@+id/appbar_spotify"
|
app:layout_anchor="@+id/appbar"
|
||||||
app:layout_anchorGravity="bottom|center"
|
app:layout_anchorGravity="bottom|center"
|
||||||
app:maxImageSize="38dp"
|
app:maxImageSize="38dp"
|
||||||
app:rippleColor="@color/colorPrimaryDark"
|
app:rippleColor="@color/colorPrimaryDark"
|
||||||
@ -59,7 +59,7 @@
|
|||||||
app:tint="@null" />
|
app:tint="@null" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/appbar_spotify"
|
android:id="@+id/appbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="280dp">
|
android:layout_height="280dp">
|
||||||
|
|
||||||
@ -72,14 +72,14 @@
|
|||||||
app:toolbarId="@+id/toolbar">
|
app:toolbarId="@+id/toolbar">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/TopLayout_spotify"
|
android:id="@+id/topLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
android:foreground="@drawable/gradient"
|
android:foreground="@drawable/gradient"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/spotify_cover_image"
|
android:id="@+id/cover_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="28dp"
|
android:layout_marginTop="28dp"
|
||||||
@ -89,13 +89,13 @@
|
|||||||
android:src="@drawable/spotify_download"
|
android:src="@drawable/spotify_download"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:layout_collapseMode="parallax"
|
app:layout_collapseMode="parallax"
|
||||||
app:layout_constraintBottom_toTopOf="@id/title_view_spotify"
|
app:layout_constraintBottom_toTopOf="@id/title_view"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/StatusBar_spotify"
|
android:id="@+id/statusBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
@ -113,11 +113,11 @@
|
|||||||
android:textColor="@color/grey"
|
android:textColor="@color/grey"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/spotify_cover_image"
|
app:layout_constraintBottom_toTopOf="@+id/cover_image"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/title_view_spotify"
|
android:id="@+id/title_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
@ -142,7 +142,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/track_list_spotify"
|
android:id="@+id/track_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingTop="26dp"
|
android:paddingTop="26dp"
|
||||||
@ -152,16 +152,7 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" />
|
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/webView_spotify"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="300dp"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_anchorGravity="bottom"/>
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</layout>
|
|
@ -16,97 +16,84 @@
|
|||||||
~ You should have received a copy of the GNU General Public License
|
~ You should have received a copy of the GNU General Public License
|
||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="#000000">
|
||||||
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<ImageView
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
android:id="@+id/imageUrl"
|
||||||
|
android:layout_width="100dp"
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="track"
|
|
||||||
type="com.shabinder.spotiflyer.models.Track" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:background="#000000"
|
android:contentDescription="Track Image"
|
||||||
android:layout_marginBottom="12dp"
|
android:scaleType="centerInside"
|
||||||
>
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/artist"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_song_placeholder" />
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/imageUrl"
|
android:id="@+id/track_name"
|
||||||
android:layout_width="100dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="Track Image"
|
android:layout_marginStart="8dp"
|
||||||
android:scaleType="centerInside"
|
android:layout_marginTop="14dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:fontFamily="@font/raleway_semibold"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/artist"
|
android:letterSpacing="0.04"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:lines="1"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:text="The Spectre"
|
||||||
app:srcCompat="@drawable/ic_song_placeholder" />
|
android:textAllCaps="false"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
|
||||||
|
android:textColor="#9AB3FF"
|
||||||
|
android:textSize="20sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/artist"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/track_name"
|
android:id="@+id/artist"
|
||||||
android:layout_width="0dp"
|
style="@style/TextAppearance.AppCompat.Body2"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="14dp"
|
android:layout_marginStart="12dp"
|
||||||
android:fontFamily="@font/raleway_semibold"
|
android:layout_marginTop="8dp"
|
||||||
android:letterSpacing="0.04"
|
android:layout_marginBottom="8dp"
|
||||||
android:lines="1"
|
android:paddingLeft="9dp"
|
||||||
android:text="The Spectre"
|
android:text="Alan Walker"
|
||||||
android:textAllCaps="false"
|
android:textSize="12sp"
|
||||||
android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:textColor="#9AB3FF"
|
app:layout_constraintEnd_toStartOf="@+id/duration"
|
||||||
android:textSize="20sp"
|
app:layout_constraintStart_toEndOf="@+id/imageUrl"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
app:layout_constraintTop_toBottomOf="@+id/track_name" />
|
||||||
app:layout_constraintStart_toStartOf="@+id/artist"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/artist"
|
|
||||||
style="@style/TextAppearance.AppCompat.Body2"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:paddingLeft="9dp"
|
|
||||||
android:text="Alan Walker"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/duration"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/imageUrl"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/track_name" />
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/duration"
|
android:id="@+id/duration"
|
||||||
style="@style/TextAppearance.AppCompat.Body2"
|
style="@style/TextAppearance.AppCompat.Body2"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="3dp"
|
android:layout_marginEnd="3dp"
|
||||||
android:paddingLeft="9dp"
|
android:paddingLeft="9dp"
|
||||||
android:text="4 minutes, 20 sec"
|
android:text="4 minutes, 20 sec"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/artist"
|
app:layout_constraintBottom_toBottomOf="@+id/artist"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||||
app:layout_constraintStart_toEndOf="@+id/artist"
|
app:layout_constraintStart_toEndOf="@+id/artist"
|
||||||
app:layout_constraintTop_toTopOf="@+id/artist" />
|
app:layout_constraintTop_toTopOf="@+id/artist" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_download"
|
android:id="@+id/btn_download"
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:backgroundTint="@color/black"
|
android:backgroundTint="@color/black"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_arrow" />
|
app:srcCompat="@drawable/ic_arrow" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</layout>
|
|
||||||
|
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Copyright (C) 2020 Shabinder Singh
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
||||||
android:id="@+id/main_youtube"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginTop="25dp"
|
|
||||||
android:fitsSystemWindows="true">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
|
||||||
android:id="@+id/btn_download_all_youtube"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="44dp"
|
|
||||||
android:background="@drawable/btn_design"
|
|
||||||
android:drawableEnd="@drawable/ic_arrow_slim"
|
|
||||||
android:drawablePadding="4dp"
|
|
||||||
android:drawableTint="@color/black"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:text="Download All |"
|
|
||||||
android:textColor="@color/black"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_anchor="@+id/appbar_youtube"
|
|
||||||
app:layout_anchorGravity="bottom|center" />
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/downloading_fab_youtube"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:backgroundTint="@color/black"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:borderWidth="0dp"
|
|
||||||
app:layout_anchor="@+id/appbar_youtube"
|
|
||||||
app:layout_anchorGravity="bottom|center"
|
|
||||||
app:maxImageSize="38dp"
|
|
||||||
app:rippleColor="@color/colorPrimaryDark"
|
|
||||||
app:srcCompat="@drawable/ic_refresh"
|
|
||||||
app:tint="@null" />
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/appbar_youtube"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="230dp">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:contentScrim="#F2C102B7"
|
|
||||||
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
|
|
||||||
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
|
|
||||||
app:toolbarId="@+id/toolbar">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/TopLayout_youtube"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:foreground="@drawable/gradient"
|
|
||||||
>
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/youtube_cover_image"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginTop="23dp"
|
|
||||||
android:layout_marginBottom="3dp"
|
|
||||||
android:contentDescription="Album Cover"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:src="@drawable/spotify_download"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_collapseMode="parallax"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/title_view_youtube"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/StatusBar_youtube"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="2dp"
|
|
||||||
android:background="@drawable/text_background_accented"
|
|
||||||
android:fontFamily="@font/raleway_semibold"
|
|
||||||
android:foreground="@drawable/rounded_gradient"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingLeft="12dp"
|
|
||||||
android:paddingTop="1dp"
|
|
||||||
android:paddingRight="12dp"
|
|
||||||
android:paddingBottom="1dp"
|
|
||||||
android:text="Total: 100 Processed: 50"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textColor="@color/grey"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/youtube_cover_image"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/title_view_youtube"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:layout_marginBottom="20dp"
|
|
||||||
android:background="#00000000"
|
|
||||||
android:fontFamily="@font/raleway_semibold"
|
|
||||||
android:gravity="end"
|
|
||||||
android:text='"SpotiFlyer"'
|
|
||||||
android:textAlignment="viewEnd"
|
|
||||||
android:textColor="#9AB3FF"
|
|
||||||
android:textSize="28sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/track_list_youtube"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:paddingTop="26dp"
|
|
||||||
android:visibility="visible"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/appbar_youtube" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
</layout>
|
|
@ -22,15 +22,6 @@
|
|||||||
android:id="@+id/navigation"
|
android:id="@+id/navigation"
|
||||||
app:startDestination="@id/mainFragment">
|
app:startDestination="@id/mainFragment">
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/spotifyFragment"
|
|
||||||
android:name="com.shabinder.spotiflyer.ui.spotify.SpotifyFragment"
|
|
||||||
android:label="main_fragment"
|
|
||||||
tools:layout="@layout/spotify_fragment" >
|
|
||||||
<argument
|
|
||||||
android:name="link"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/mainFragment"
|
android:id="@+id/mainFragment"
|
||||||
android:name="com.shabinder.spotiflyer.ui.mainfragment.MainFragment"
|
android:name="com.shabinder.spotiflyer.ui.mainfragment.MainFragment"
|
||||||
@ -51,16 +42,11 @@
|
|||||||
app:destination="@id/downloadRecord"
|
app:destination="@id/downloadRecord"
|
||||||
app:enterAnim="@android:anim/slide_in_left"
|
app:enterAnim="@android:anim/slide_in_left"
|
||||||
app:exitAnim="@android:anim/slide_out_right" />
|
app:exitAnim="@android:anim/slide_out_right" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_mainFragment_to_gaanaFragment"
|
||||||
|
app:destination="@id/gaanaFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
|
||||||
android:id="@+id/youtubeFragment"
|
|
||||||
android:name="com.shabinder.spotiflyer.ui.youtube.YoutubeFragment"
|
|
||||||
android:label="YoutubeFragment"
|
|
||||||
tools:layout="@layout/youtube_fragment">
|
|
||||||
<argument
|
|
||||||
android:name="link"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/downloadRecord"
|
android:id="@+id/downloadRecord"
|
||||||
android:name="com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragment"
|
android:name="com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragment"
|
||||||
@ -76,5 +62,35 @@
|
|||||||
app:destination="@id/youtubeFragment"
|
app:destination="@id/youtubeFragment"
|
||||||
app:enterAnim="@android:anim/slide_in_left"
|
app:enterAnim="@android:anim/slide_in_left"
|
||||||
app:exitAnim="@android:anim/slide_out_right"/>
|
app:exitAnim="@android:anim/slide_out_right"/>
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_downloadRecord_to_gaanaFragment"
|
||||||
|
app:destination="@id/gaanaFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/spotifyFragment"
|
||||||
|
android:name="com.shabinder.spotiflyer.ui.spotify.SpotifyFragment"
|
||||||
|
android:label="main_fragment"
|
||||||
|
tools:layout="@layout/track_list_fragment" >
|
||||||
|
<argument
|
||||||
|
android:name="link"
|
||||||
|
app:argType="string" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/youtubeFragment"
|
||||||
|
android:name="com.shabinder.spotiflyer.ui.youtube.YoutubeFragment"
|
||||||
|
android:label="YoutubeFragment"
|
||||||
|
tools:layout="@layout/track_list_fragment">
|
||||||
|
<argument
|
||||||
|
android:name="link"
|
||||||
|
app:argType="string" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/gaanaFragment"
|
||||||
|
android:name="com.shabinder.spotiflyer.ui.gaana.GaanaFragment"
|
||||||
|
android:label="GaanaFragment"
|
||||||
|
tools:layout="@layout/track_list_fragment">
|
||||||
|
<argument
|
||||||
|
android:name="link"
|
||||||
|
app:argType="string" />
|
||||||
</fragment>
|
</fragment>
|
||||||
</navigation>
|
</navigation>
|
@ -20,10 +20,12 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#FC5C7D</color>
|
<color name="colorPrimary">#FC5C7D</color>
|
||||||
<color name="colorPrimaryDark">#CE1CFF</color>
|
<color name="colorPrimaryDark">#CE1CFF</color>
|
||||||
<color name="colorAccent">#799BFF</color>
|
<color name="colorAccent">#9AB3FF</color>
|
||||||
<color name="white">#FFFFFF</color>
|
<color name="white">#FFFFFF</color>
|
||||||
<color name="grey">#99FFFFFF</color>
|
<color name="grey">#99FFFFFF</color>
|
||||||
<color name="black">#000000</color>
|
<color name="black">#000000</color>
|
||||||
|
<color name="dark">#121212</color>
|
||||||
|
<color name="successGreen">#59C351</color>
|
||||||
|
<color name="errorRed">#FF9494</color>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -19,37 +19,89 @@
|
|||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimaryDark">#000000</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="android:colorBackground">@color/black</item>
|
||||||
|
<item name="colorOnBackground">@color/white</item>
|
||||||
|
<item name="colorError">#FF5E5E</item>
|
||||||
|
<item name="colorOnError">@color/black</item>
|
||||||
|
<item name="colorSurface">@color/dark</item>
|
||||||
|
<item name="statusBarScrim">@color/black</item>
|
||||||
|
<item name="android:statusBarColor">@color/black</item>
|
||||||
|
<item name="colorOnSurface">@color/white</item>
|
||||||
<item name="colorPrimary">#FC5C7D</item>
|
<item name="colorPrimary">#FC5C7D</item>
|
||||||
<item name="android:background">#000000</item>
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<item name="colorSecondary">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorOnSecondary">@color/white</item>
|
||||||
<item name="android:textColor">#FFFFFF</item>
|
<item name="android:textColor">#FFFFFF</item>
|
||||||
<item name="colorAccent">#6A82FB</item>
|
<item name="android:textColorHint">@color/grey</item>
|
||||||
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">#A9B200FF</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
<item name="android:radius">11dp</item>
|
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">@color/colorPrimaryDark</item>
|
||||||
|
<item name="android:radius">12dp</item>
|
||||||
<!-- Text Appearances !-->
|
<!-- Text Appearances !-->
|
||||||
<!-- use our brand's custom TextAppearance4 !-->
|
<!-- use our brand's custom TextAppearance4 !-->
|
||||||
<item name="textAppearanceHeadline4">@style/TextAppearance.AppTheme.Headline4</item>
|
<item name="textAppearanceHeadline4">@style/TextAppearance.AppTheme.Headline4</item>
|
||||||
<!-- use default Body2 text apperance !-->
|
<!-- use default Body2 text appearance !-->
|
||||||
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
|
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.AppCompat.Button.Colored.Gradient" parent="Widget.AppCompat.ActionButton">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:background">@drawable/btn_design</item>
|
||||||
|
<item name="android:textColor">@color/black</item>
|
||||||
|
<item name="android:textAllCaps">true</item>
|
||||||
|
<item name="android:textAlignment">center</item>
|
||||||
|
<item name="android:paddingLeft">4dp</item>
|
||||||
|
<item name="android:paddingRight">4dp</item>
|
||||||
|
<item name="android:textSize">16sp</item>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.AppCompat.ImageButton.40dp" parent="Widget.AppCompat.ImageButton">
|
||||||
|
<item name="android:layout_width">40dp</item>
|
||||||
|
<item name="android:layout_height">40dp</item>
|
||||||
|
<item name="android:background">@drawable/transparent</item>
|
||||||
|
<item name="android:scaleType">fitCenter</item>
|
||||||
|
<item name="android:layout_margin">8dp</item>
|
||||||
|
</style>
|
||||||
|
<style name="Widget.AppCompat.TextView.Gradient">
|
||||||
|
<item name="android:background">@drawable/text_background_accented</item>
|
||||||
|
<item name="android:textAlignment">center</item>
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:inputType">text</item>
|
||||||
|
<item name="android:gravity">center</item>
|
||||||
|
<item name="android:layout_gravity">center</item>
|
||||||
|
<item name="android:padding">8dp</item>
|
||||||
|
<item name="android:textSize">18sp</item>
|
||||||
|
<item name="android:textColor">@color/white</item>
|
||||||
|
</style>
|
||||||
|
<style name="Widget.AppCompat.ImageButton.platformIcon" parent="Widget.AppCompat.ImageButton">
|
||||||
|
<item name="android:layout_width">48dp</item>
|
||||||
|
<item name="android:layout_height">48dp</item>
|
||||||
|
<item name="android:background">@color/black</item>
|
||||||
|
<item name="android:scaleType">fitCenter</item>
|
||||||
|
<item name="android:padding">4dp</item>
|
||||||
|
</style>
|
||||||
<style name="AlertDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog.Alert">
|
<style name="AlertDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog.Alert">
|
||||||
<item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item>
|
<item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item>
|
||||||
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
|
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
|
||||||
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
|
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
|
||||||
|
<item name="android:textSize">22sp</item>
|
||||||
|
<item name="fontFamily">@font/amita</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent">
|
<style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent">
|
||||||
<item name="background">@color/white</item>
|
<item name="background">@color/white</item>
|
||||||
<item name="cornerFamily">rounded</item>
|
<item name="cornerFamily">rounded</item>
|
||||||
<item name="cornerSize">20dp</item>
|
<item name="cornerSize">8dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton">
|
<style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton">
|
||||||
<item name="backgroundTint">@color/colorPrimary</item>
|
<item name="backgroundTint">@color/colorPrimary</item>
|
||||||
<item name="rippleColor">@color/colorPrimaryDark</item>
|
<item name="rippleColor">@color/cardview_dark_background</item>
|
||||||
<item name="android:textColor">@android:color/black</item>
|
<item name="android:textColor">@android:color/black</item>
|
||||||
<item name="android:textSize">14sp</item>
|
<item name="android:textSize">14sp</item>
|
||||||
|
<item name="android:layout_marginEnd">4dp</item>
|
||||||
|
<item name="android:layout_marginBottom">2dp</item>
|
||||||
<item name="android:textAllCaps">false</item>
|
<item name="android:textAllCaps">false</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<AppUpdater>
|
<AppUpdater>
|
||||||
<update>
|
<update>
|
||||||
<latestVersion>1.5.1</latestVersion>
|
<latestVersion>1.6</latestVersion>
|
||||||
<latestVersionCode>7</latestVersionCode>
|
<latestVersionCode>8</latestVersionCode>
|
||||||
<url>https://github.com/Shabinder/SpotiFlyer/releases/download/1.5/SpotiFlyer-v1.5.apk</url>
|
<url>https://github.com/Shabinder/SpotiFlyer/releases/</url>
|
||||||
</update>
|
</update>
|
||||||
</AppUpdater>
|
</AppUpdater>
|
@ -20,7 +20,7 @@ buildscript {
|
|||||||
ext{
|
ext{
|
||||||
kotlin_version = "1.4.10"
|
kotlin_version = "1.4.10"
|
||||||
navigationVersion = '2.3.0'
|
navigationVersion = '2.3.0'
|
||||||
ext.hilt_version = '2.28-alpha'
|
ext.hilt_version = '2.29.1-alpha'
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@ -33,7 +33,7 @@ buildscript {
|
|||||||
//safe-Args
|
//safe-Args
|
||||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
||||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||||
// classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user