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">
|
||||
<dictionary name="shabinder">
|
||||
<words>
|
||||
<w>albumseokey</w>
|
||||
<w>amita</w>
|
||||
<w>cardview</w>
|
||||
<w>cherrypick</w>
|
||||
<w>downloadrecord</w>
|
||||
<w>emoji</w>
|
||||
<w>ffmpeg</w>
|
||||
<w>flyer</w>
|
||||
<w>gaana</w>
|
||||
<w>gener</w>
|
||||
<w>hqdefault</w>
|
||||
<w>insta</w>
|
||||
<w>instagram</w>
|
||||
<w>jetbrains</w>
|
||||
<w>kotlinx</w>
|
||||
<w>linkedin</w>
|
||||
<w>mainfragment</w>
|
||||
<w>maxresdefault</w>
|
||||
<w>moshi</w>
|
||||
@ -17,14 +25,17 @@
|
||||
<w>musicplaceholder</w>
|
||||
<w>raleway</w>
|
||||
<w>semibold</w>
|
||||
<w>seokey</w>
|
||||
<w>shabinder</w>
|
||||
<w>singh</w>
|
||||
<w>snackbar</w>
|
||||
<w>spoti</w>
|
||||
<w>spotiflyer</w>
|
||||
<w>spotify</w>
|
||||
<w>spotifydownloader</w>
|
||||
<w>spotifyler</w>
|
||||
<w>thru</w>
|
||||
<w>weyfdnx</w>
|
||||
<w>youtu</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
@ -21,22 +21,23 @@ apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
//apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
buildFeatures{
|
||||
dataBinding = true
|
||||
//dataBinding = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.shabinder.spotiflyer'
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 29
|
||||
versionCode 7
|
||||
versionName "1.5.1"
|
||||
versionCode 8
|
||||
versionName "1.6"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
packagingOptions {
|
||||
@ -90,8 +91,10 @@ dependencies {
|
||||
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-android:1.4.0'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
|
||||
|
||||
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"
|
||||
implementation "androidx.room:room-ktx:2.2.5"
|
||||
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-kotlin:1.11.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.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 '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 {
|
||||
# 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
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
@ -21,24 +21,24 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
import com.github.javiersantos.appupdater.AppUpdater
|
||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest
|
||||
import com.shabinder.spotiflyer.utils.createDirectories
|
||||
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
@ -49,49 +49,48 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* This is App's God Activity
|
||||
* */
|
||||
@Suppress("DEPRECATION")
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity(){
|
||||
private var spotifyService : SpotifyService? = null
|
||||
private var isConnected: Boolean = false
|
||||
private var sharedPref :SharedPreferences? = null
|
||||
private var token :String =""
|
||||
private lateinit var binding: MainActivityBinding
|
||||
lateinit var snackBarAnchor: View
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
|
||||
private lateinit var navController: NavController
|
||||
@Inject lateinit var moshi: Moshi
|
||||
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
||||
//Enabling Dark Mode
|
||||
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!
|
||||
SpotifyDownloadHelper.startService(this)
|
||||
|
||||
if(sharedViewModel.spotifyService.value == null){
|
||||
authenticateSpotify()
|
||||
}else{
|
||||
implementSpotifyService(sharedViewModel.accessToken.value!!)
|
||||
}
|
||||
authenticateSpotify()
|
||||
|
||||
requestPermission()
|
||||
disableDozeMode()
|
||||
checkIfLatestVersion()
|
||||
createDirectories()
|
||||
isConnected = sharedViewModel.isOnline(this)
|
||||
sharedViewModel.isConnected.value = isConnected
|
||||
Log.i("Connection Status", isConnected.toString())
|
||||
Log.i("Connection Status", isOnline().toString())
|
||||
|
||||
//starting Notification and Downloader Service!
|
||||
startService(this)
|
||||
|
||||
handleIntentFromExternalActivity()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: 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)
|
||||
}
|
||||
|
||||
@ -102,9 +101,10 @@ class MainActivity : AppCompatActivity(){
|
||||
this.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
|
||||
if (!isIgnoringBatteryOptimizations) {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
intent.data = Uri.parse("package:$packageName")
|
||||
val intent = Intent().apply{
|
||||
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
startActivityForResult(intent, 1233)
|
||||
}
|
||||
}
|
||||
@ -139,13 +139,13 @@ class MainActivity : AppCompatActivity(){
|
||||
"Bearer $token"
|
||||
).build()
|
||||
chain.proceed(request)
|
||||
})
|
||||
}).addInterceptor(NetworkInterceptor())
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://api.spotify.com/v1/")
|
||||
.client(httpClient.build())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
.baseUrl("https://api.spotify.com/v1/")
|
||||
.client(httpClient.build())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
|
||||
spotifyService = retrofit.create(SpotifyService::class.java)
|
||||
sharedViewModel.spotifyService.value = spotifyService
|
||||
@ -154,16 +154,13 @@ class MainActivity : AppCompatActivity(){
|
||||
|
||||
fun authenticateSpotify() {
|
||||
sharedViewModel.uiScope.launch {
|
||||
if (isConnected) {
|
||||
Log.i("Post Request", "Made")
|
||||
token = spotifyServiceTokenRequest.getToken()!!.access_token
|
||||
implementSpotifyService(token)
|
||||
Log.i("Post Request", token)
|
||||
sharedViewModel.accessToken.value = token
|
||||
}else{
|
||||
Log.i("network", "unavailable")
|
||||
// sharedViewModel.showAlertDialog(resources,this@MainActivity)
|
||||
Log.i("Spotify Authentication","Started")
|
||||
val token = spotifyServiceTokenRequest.getToken()
|
||||
token.value?.let {
|
||||
showMessage("Success: Spotify Token Acquired",isSuccess = true)
|
||||
implementSpotifyService(it.access_token)
|
||||
}
|
||||
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() {
|
||||
val appUpdater = AppUpdater(this)
|
||||
.showAppUpdated(false)//true:Show App is Update Dialog
|
||||
.setUpdateFrom(UpdateFrom.XML)
|
||||
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
|
||||
.setCancelable(false)
|
||||
.setButtonDoNotShowAgain("Remind Later")
|
||||
.setButtonDoNotShowAgainClickListener { dialog, _ -> dialog.dismiss() }
|
||||
.setButtonUpdateClickListener { _, _ ->
|
||||
val uri: Uri =
|
||||
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
|
||||
@ -220,11 +206,10 @@ class MainActivity : AppCompatActivity(){
|
||||
}
|
||||
|
||||
companion object{
|
||||
private var instance = MainActivity()
|
||||
fun getInstance():MainActivity{
|
||||
return instance
|
||||
}
|
||||
private lateinit var instance: MainActivity
|
||||
fun getInstance():MainActivity = instance
|
||||
}
|
||||
|
||||
init {
|
||||
instance = this
|
||||
}
|
||||
|
@ -17,49 +17,22 @@
|
||||
|
||||
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.ViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.networking.SpotifyService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import java.io.File
|
||||
|
||||
class SharedViewModel : ViewModel() {
|
||||
var intentString = MutableLiveData<String>().apply { value = "" }
|
||||
var intentString = MutableLiveData<String>()
|
||||
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()
|
||||
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import android.widget.Toast
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
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
|
||||
|
||||
object YTDownloadHelper {
|
||||
var context : Context? = null
|
||||
var statusBar: TextView? = null
|
||||
|
||||
fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) {
|
||||
format?.let {
|
||||
val url:String = format.url()
|
||||
// Log.i("DHelper Link Found", url)
|
||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars(
|
||||
ytTrack.name!!
|
||||
) +".m4a")
|
||||
suspend fun downloadYTTracks(
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
tracks:List<TrackDetails>,
|
||||
){
|
||||
val downloadList = ArrayList<DownloadObject>()
|
||||
tracks.forEach {
|
||||
if(!isOnline()){
|
||||
showNoConnectionAlert()
|
||||
return
|
||||
}
|
||||
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(
|
||||
track = ytTrack,
|
||||
url = url,
|
||||
outputDir = outputFile
|
||||
trackDetails = it,
|
||||
ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
|
||||
.substringBeforeLast("."),
|
||||
outputFile = outputFile
|
||||
)
|
||||
Log.i("DH",outputFile)
|
||||
startService(context!!, downloadObject)
|
||||
statusBar?.visibility= View.VISIBLE
|
||||
|
||||
downloadList.add(downloadObject)
|
||||
}
|
||||
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
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
@Parcelize
|
||||
data class DownloadObject(
|
||||
var ytVideo: YTTrack?=null,
|
||||
var track: Track?=null,
|
||||
var url:String,
|
||||
var outputDir:String
|
||||
var trackDetails: TrackDetails,
|
||||
var ytVideoId:String,
|
||||
var outputFile:String
|
||||
):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
|
||||
|
||||
@Parcelize
|
||||
data class YTTrack(
|
||||
var id:String?,
|
||||
var title:String?,
|
||||
var duration:Int?,
|
||||
var author:String?,
|
||||
var viewCount:Long?,
|
||||
var thumbnails:List<String?>?
|
||||
data class YoutubeTrack(
|
||||
var name: String? = null,
|
||||
var type: String? = null, // Song / Video
|
||||
var artist: String? = null,
|
||||
var duration:String? = null,
|
||||
var videoId: String? = null
|
||||
):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/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
package com.shabinder.spotiflyer.models.spotify
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 com.squareup.moshi.Json
|
@ -15,7 +15,7 @@
|
||||
* 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 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/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
package com.shabinder.spotiflyer.models.spotify
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
@ -15,9 +15,10 @@
|
||||
* 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 com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@ -31,7 +32,6 @@ data class Track(
|
||||
var explicit: Boolean? = null,
|
||||
var external_urls: Map<String?, String?>? = null,
|
||||
var href: String? = null,
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var preview_url: String? = null,
|
||||
var track_number: Int = 0,
|
||||
@ -40,5 +40,6 @@ data class Track(
|
||||
var album: Album? = null,
|
||||
var external_ids: Map<String?, String?>? = null,
|
||||
var popularity: Int? = null,
|
||||
var ytCoverUrl:String? = null,
|
||||
var downloaded:String? = "notDownloaded"):Parcelable
|
||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||
):Parcelable
|
||||
|
@ -15,7 +15,7 @@
|
||||
* 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 kotlinx.android.parcel.Parcelize
|
@ -15,7 +15,7 @@
|
||||
* 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 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/>.
|
||||
*/
|
||||
|
||||
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.*
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
@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")
|
||||
suspend fun getPlaylistTracks(
|
||||
@Path("playlist_id") playlistId: String?,
|
||||
@Query("offset") offset: Int = 0,
|
||||
@Query("limit") limit: Int = 100
|
||||
): PagingObjectPlaylistTrack
|
||||
): Optional<PagingObjectPlaylistTrack>
|
||||
|
||||
@GET("tracks/{id}")
|
||||
suspend fun getTrack(@Path("id") trackId: String?): Track
|
||||
suspend fun getTrack(@Path("id") trackId: String?): Optional<Track>
|
||||
|
||||
@GET("episodes/{id}")
|
||||
suspend fun getEpisode(@Path("id") episodeId: String?): Track
|
||||
suspend fun getEpisode(@Path("id") episodeId: String?): Optional<Track>
|
||||
|
||||
@GET("shows/{id}")
|
||||
suspend fun getShow(@Path("id") showId: String?): Track
|
||||
suspend fun getShow(@Path("id") showId: String?): Optional<Track>
|
||||
|
||||
@GET("albums/{id}")
|
||||
suspend fun getAlbum(@Path("id") albumId: String?): Album
|
||||
suspend fun getAlbum(@Path("id") albumId: String?): Optional<Album>
|
||||
}
|
||||
|
||||
interface SpotifyServiceTokenRequest{
|
||||
|
||||
@POST("api/token")
|
||||
@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 com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -34,30 +35,43 @@ import kotlinx.coroutines.launch
|
||||
class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.ViewHolder>(DownloadRecordDiffCallback()) {
|
||||
|
||||
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 {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding =DownloadRecordItemBinding.inflate(layoutInflater)
|
||||
val binding = DownloadRecordItemBinding.inflate(layoutInflater)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
adapterScope.launch {
|
||||
bindImage(holder.binding.coverUrl,item.coverUrl)
|
||||
bindImage(holder.binding.coverUrl,item.coverUrl,source)
|
||||
}
|
||||
holder.binding.itemName.text = item.name
|
||||
holder.binding.totalItems.text = "Tracks: ${item.totalFiles}"
|
||||
holder.binding.type.text = item.type
|
||||
holder.binding.btnAction.setOnClickListener {
|
||||
if (item.link.contains("spotify",true)){
|
||||
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link)))
|
||||
}else if(item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) ){
|
||||
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link))
|
||||
}
|
||||
when {
|
||||
item.link.contains("spotify",true) -> {
|
||||
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((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)
|
||||
|
||||
fun submitList(list: MutableList<DownloadRecord>?,source: Source) {
|
||||
super.submitList(list)
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
|
@ -21,12 +21,11 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@ -41,43 +40,49 @@ class DownloadRecordFragment : Fragment() {
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): 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)
|
||||
adapter = DownloadRecordAdapter()
|
||||
binding.downloadRecordList.adapter = adapter
|
||||
|
||||
downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, {
|
||||
if(it.isNotEmpty()){
|
||||
downloadRecordViewModel.spotifyList = mutableListOf()
|
||||
downloadRecordViewModel.ytList = mutableListOf()
|
||||
resetLists()
|
||||
for (downloadRecord in it) {
|
||||
if(downloadRecord.link.contains("spotify",true)) downloadRecordViewModel.spotifyList.add(downloadRecord)
|
||||
else downloadRecordViewModel.ytList.add(downloadRecord)
|
||||
when{
|
||||
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 {
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
if(tab?.text == "Spotify"){
|
||||
adapter.submitList(downloadRecordViewModel.spotifyList)
|
||||
} else adapter.submitList(downloadRecordViewModel.ytList)
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
// Handle tab reselect
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
// Handle tab unselected
|
||||
when(tab?.position){
|
||||
0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify)
|
||||
1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana)
|
||||
2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube)
|
||||
}
|
||||
}
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
|
||||
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 val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob)
|
||||
var spotifyList = mutableListOf<DownloadRecord>()
|
||||
var gaanaList = mutableListOf<DownloadRecord>()
|
||||
var ytList = mutableListOf<DownloadRecord>()
|
||||
val downloadRecordList = MutableLiveData<MutableList<DownloadRecord>>().apply {
|
||||
value = mutableListOf()
|
||||
@ -40,6 +41,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data
|
||||
init {
|
||||
getDownloadRecordList()
|
||||
}
|
||||
|
||||
private fun getDownloadRecordList() {
|
||||
uiScope.launch {
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shreyaspatil.easyupipayment.EasyUpiPayment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -53,123 +50,88 @@ class MainFragment : Fragment() {
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
||||
binding = MainFragmentBinding.inflate(inflater,container,false)
|
||||
initializeAll()
|
||||
|
||||
binding.btnSearch.setOnClickListener {
|
||||
if(!isOnline()){
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
val link = binding.linkSearch.text.toString()
|
||||
if (link.contains("spotify",true)){
|
||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link))
|
||||
}else if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){
|
||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
|
||||
}else{Toast.makeText(context,"Link is Not Valid",Toast.LENGTH_SHORT).show()}
|
||||
when{
|
||||
//SPOTIFY
|
||||
link.contains("spotify",true) -> {
|
||||
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
||||
(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()
|
||||
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!
|
||||
**/
|
||||
private fun handleIntent() {
|
||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{
|
||||
if(it != ""){
|
||||
sharedViewModel.uiScope.launch(Dispatchers.IO) {
|
||||
while (sharedViewModel.accessToken.value == "") {
|
||||
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
|
||||
sharedViewModel.uiScope.launch(Dispatchers.IO) {
|
||||
//Wait for any Authentication to Finish ,
|
||||
// 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
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
withContext(Dispatchers.Main){
|
||||
binding.linkSearch.setText(sharedViewModel.intentString.value)
|
||||
binding.btnSearch.performClick()
|
||||
sharedViewModel.intentString.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main){
|
||||
binding.linkSearch.setText(sharedViewModel.intentString.value)
|
||||
binding.btnSearch.performClick()
|
||||
//Intent Consumed
|
||||
sharedViewModel.intentString.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementing buttons
|
||||
**/
|
||||
private fun openSpotifyButton() {
|
||||
val manager: PackageManager = requireActivity().packageManager
|
||||
try {
|
||||
val i = manager.getLaunchIntentForPackage("com.spotify.music")
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
i.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
binding.btnSpotify.setOnClickListener { startActivity(i) }
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
val uri: Uri =
|
||||
Uri.parse("http://open.spotify.com")
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
binding.btnSpotify.setOnClickListener {
|
||||
startActivity(intent)
|
||||
private fun initializeAll() {
|
||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
binding.apply {
|
||||
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
|
||||
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
|
||||
btnYoutube.openPlatformOnClick("com.google.android.youtube","http://m.youtube.com")
|
||||
btnGithub.openPlatformOnClick("http://github.com/Shabinder/SpotiFlyer")
|
||||
btnInsta.openPlatformOnClick("http://www.instagram.com/mr.shabinder")
|
||||
btnHistory.setOnClickListener {
|
||||
findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
|
||||
}
|
||||
usage.text = usageText()
|
||||
btnDonate.setOnClickListener {
|
||||
easyUpiPayment.startPayment()
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return SpannableStringBuilder()
|
||||
.append(getText(R.string.d_one)).append("\n")
|
||||
|
@ -18,307 +18,124 @@
|
||||
package com.shabinder.spotiflyer.ui.spotify
|
||||
|
||||
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.Environment
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
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.shabinder.spotiflyer.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 com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.utils.*
|
||||
import com.shabinder.spotiflyer.utils.Provider.mainActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SpotifyFragment : Fragment() {
|
||||
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
|
||||
class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() {
|
||||
|
||||
@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")
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater,R.layout.spotify_fragment,container,false)
|
||||
adapterSpotify = SpotifyTrackListAdapter()
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
initializeAll()
|
||||
initializeLiveDataObservers()
|
||||
initializeBroadcast()
|
||||
|
||||
val args = SpotifyFragmentArgs.fromBundle(requireArguments())
|
||||
val spotifyLink = args.link
|
||||
val spotifyLink = args.link.substringAfter("open.spotify.com/")
|
||||
|
||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||
|
||||
Log.i("Fragment", "$type : $link")
|
||||
Log.i("Spotify Fragment", "$type : $link")
|
||||
|
||||
|
||||
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
||||
(activity as MainActivity).authenticateSpotify()
|
||||
if(isOnline()) mainActivity.authenticateSpotify()
|
||||
}
|
||||
if(!isOnline()){//Device Offline
|
||||
sharedViewModel.showAlertDialog(resources,requireContext())
|
||||
}else if (type == "Error" || link == "Error") {//Incorrect Link
|
||||
showToast("Please Check Your Link!")
|
||||
}else if(spotifyLink.contains("open.spotify",true)){//Link Validation!!
|
||||
if(type == "episode" || type == "show"){//TODO Implementation
|
||||
showToast("Implementing Soon, Stay Tuned!")
|
||||
|
||||
when{
|
||||
type == "Error" || link == "Error" -> {
|
||||
showMessage("Please Check Your Link!")
|
||||
mainActivity.onBackPressed()
|
||||
}
|
||||
else{
|
||||
spotifyViewModel.spotifySearch(type,link)
|
||||
if(type=="album")adapterSpotify.isAlbum = true
|
||||
|
||||
binding.btnDownloadAllSpotify.setOnClickListener {
|
||||
for (track in spotifyViewModel.trackList.value!!){
|
||||
if(track.downloaded != "Downloaded"){
|
||||
track.downloaded = "Downloading"
|
||||
else -> {
|
||||
if(type == "episode" || type == "show"){//TODO Implementation
|
||||
showMessage("Implementing Soon, Stay Tuned!")
|
||||
}
|
||||
else{
|
||||
this.viewModel.spotifySearch(type,link)
|
||||
|
||||
binding.btnDownloadAll.setOnClickListener {
|
||||
if(!isOnline()){
|
||||
showNoConnectionAlert()
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
binding.btnDownloadAllSpotify.visibility = View.GONE
|
||||
binding.downloadingFabSpotify.visibility = View.VISIBLE
|
||||
binding.btnDownloadAll.visibility = View.GONE
|
||||
binding.downloadingFab.visibility = View.VISIBLE
|
||||
|
||||
|
||||
rotateAnim(binding.downloadingFabSpotify)
|
||||
for (track in spotifyViewModel.trackList.value!!){
|
||||
if(track.downloaded != "Downloaded"){
|
||||
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track))
|
||||
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))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
**/
|
||||
private fun initializeAll() {
|
||||
webView = binding.webViewSpotify
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
|
||||
spotifyViewModel.spotifyService = it
|
||||
this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
|
||||
adapter = TrackListAdapter(this.viewModel)
|
||||
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
|
||||
this.viewModel.spotifyService = it
|
||||
})
|
||||
SpotifyDownloadHelper.webView = binding.webViewSpotify
|
||||
SpotifyDownloadHelper.context = requireContext()
|
||||
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel
|
||||
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify
|
||||
binding.trackListSpotify.adapter = adapterSpotify
|
||||
(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
|
||||
DownloadHelper.youtubeMusicApi = youtubeMusicApi
|
||||
DownloadHelper.sharedViewModel = sharedViewModel
|
||||
DownloadHelper.statusBar = binding.statusBar
|
||||
binding.trackList.adapter = adapter
|
||||
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
}
|
@ -17,57 +17,53 @@
|
||||
|
||||
package com.shabinder.spotiflyer.ui.spotify
|
||||
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.*
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
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 kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
|
||||
ViewModel(){
|
||||
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
|
||||
|
||||
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
|
||||
|
||||
private var viewModelJob = Job()
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
|
||||
fun spotifySearch(type:String,link: String){
|
||||
when (type) {
|
||||
"track" -> {
|
||||
uiScope.launch {
|
||||
val trackObject = getTrackDetails(link)
|
||||
folderType = "Tracks"
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
trackObject.downloaded = "Downloaded"
|
||||
}
|
||||
tempTrackList.add(trackObject)
|
||||
trackList.value = tempTrackList
|
||||
title.value = trackObject.name
|
||||
coverUrl.value = trackObject.album!!.images?.get(0)!!.url!!
|
||||
withContext(Dispatchers.IO){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Track",
|
||||
name = title.value!!,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value!!,
|
||||
totalFiles = tempTrackList.size,
|
||||
downloaded = trackObject.downloaded =="Downloaded",
|
||||
directory = finalOutputDir(trackObject.name!!,folderType,subFolder)
|
||||
))
|
||||
getTrackDetails(link)?.also {
|
||||
folderType = "Tracks"
|
||||
if(File(finalOutputDir(it.name,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
trackList.value = listOf(it).toTrackDetailsList()
|
||||
title.value = it.name
|
||||
coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url
|
||||
withContext(Dispatchers.IO){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Track",
|
||||
name = title.value!!,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value!!,
|
||||
totalFiles = 1,
|
||||
downloaded = it.downloaded == DownloadStatus.Downloaded,
|
||||
directory = finalOutputDir(it.name,folderType,subFolder)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,25 +73,23 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
val albumObject = getAlbumDetails(link)
|
||||
folderType = "Albums"
|
||||
subFolder = albumObject?.name.toString()
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
albumObject?.tracks?.items?.forEach {
|
||||
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)))
|
||||
tempTrackList.add(it)
|
||||
it.album = Album(images = listOf(Image(url = albumObject.images?.elementAtOrNull(1)?.url ?: albumObject.images?.elementAtOrNull(0)?.url )))
|
||||
}
|
||||
trackList.value = tempTrackList
|
||||
trackList.value = albumObject?.tracks?.items?.toTrackDetailsList()
|
||||
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){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Album",
|
||||
name = title.value!!,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value.toString(),
|
||||
totalFiles = tempTrackList.size,
|
||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
||||
totalFiles = trackList.value?.size ?: 0,
|
||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
||||
))
|
||||
}
|
||||
@ -112,7 +106,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
playlistObject?.tracks?.items?.forEach {
|
||||
it.track?.let {
|
||||
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
|
||||
it1.downloaded = "Downloaded"
|
||||
it1.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
tempTrackList.add(it1)
|
||||
}
|
||||
@ -128,15 +122,15 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
|
||||
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
|
||||
}
|
||||
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
|
||||
trackList.value = tempTrackList
|
||||
trackList.value = tempTrackList.toTrackDetailsList()
|
||||
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){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Playlist",
|
||||
name = title.value!!,
|
||||
name = title.value.toString(),
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl.value!!,
|
||||
coverUrl = coverUrl.value.toString(),
|
||||
totalFiles = tempTrackList.size,
|
||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
||||
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?{
|
||||
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?{
|
||||
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?{
|
||||
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?{
|
||||
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.View
|
||||
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.navigation.fragment.navArgs
|
||||
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.models.Track
|
||||
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
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 YoutubeFragment : Fragment() {
|
||||
private const val sampleDomain2 = "youtu.be"
|
||||
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
|
||||
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(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false)
|
||||
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
adapter = YoutubeTrackListAdapter()
|
||||
YTDownloadHelper.context = requireContext()
|
||||
YTDownloadHelper.statusBar = binding.StatusBarYoutube
|
||||
binding.trackListYoutube.adapter = adapter
|
||||
|
||||
initializeLiveDataObservers()
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
|
||||
adapter = TrackListAdapter(this.viewModel)
|
||||
binding.trackList.adapter = adapter
|
||||
|
||||
val args = YoutubeFragmentArgs.fromBundle(requireArguments())
|
||||
val link = args.link
|
||||
@ -70,7 +63,11 @@ class YoutubeFragment : Fragment() {
|
||||
|
||||
private fun youtubeSearch(linkSearch:String) {
|
||||
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"
|
||||
if(link.contains(sampleDomain1,true) ){
|
||||
searchId = link.substringAfterLast("=","error")
|
||||
@ -79,57 +76,48 @@ class YoutubeFragment : Fragment() {
|
||||
searchId = link.substringAfterLast("/","error")
|
||||
}
|
||||
if(searchId != "error") {
|
||||
youtubeViewModel.getYTTrack(searchId,ytDownloader)
|
||||
binding.btnDownloadAllYoutube.setOnClickListener {
|
||||
YTDownloadHelper.downloadFile(null,"YT_Downloads",
|
||||
youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value)
|
||||
this.viewModel.getYTTrack(searchId,ytDownloader)
|
||||
}else{showMessage("Your Youtube Link is not of a Video!!")}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.Artist
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.utils.finalOutputDir
|
||||
import kotlinx.coroutines.*
|
||||
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 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) :
|
||||
ViewModel(){
|
||||
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
|
||||
/*
|
||||
* 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>()
|
||||
val format = MutableLiveData<Format>()
|
||||
private val loading = "Loading"
|
||||
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
|
||||
var coverUrl = MutableLiveData<String>().apply { value = loading }
|
||||
override var folderType = "YT_Downloads"
|
||||
override var subFolder = ""
|
||||
|
||||
private var viewModelJob = Job()
|
||||
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
|
||||
|
||||
|
||||
fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) {
|
||||
uiScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
Log.i("YT View Model",searchId)
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
val detail = video?.details()
|
||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title()
|
||||
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")
|
||||
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
|
||||
if(!isOnline())return
|
||||
try{
|
||||
uiScope.launch(Dispatchers.IO) {
|
||||
Log.i("YT Playlist",searchId)
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details()
|
||||
val name = playlistDetails.title()
|
||||
subFolder = removeIllegalChars(name).toString()
|
||||
val videos = playlist.videos()
|
||||
coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg")
|
||||
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 {
|
||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
try {
|
||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
try {
|
||||
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
Log.i("YTDownloader", e.toString())
|
||||
null
|
||||
this@YoutubeViewModel.trackList.postValue(videos.map {
|
||||
TrackDetails(
|
||||
title = it.title(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArt = File(
|
||||
Environment.getExternalStorageDirectory(),
|
||||
defaultDir + ".Images/" + it.videoId() + ".jpeg"
|
||||
),
|
||||
source = Source.YouTube,
|
||||
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){
|
||||
databaseDAO.insert(DownloadRecord(
|
||||
type = "Track",
|
||||
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
|
||||
link = "https://www.youtube.com/watch?v=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg",
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
totalFiles = 1,
|
||||
downloaded = false,
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.App
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.database.DatabaseDAO
|
||||
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.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
@ -36,23 +40,35 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import java.io.File
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@InstallIn(ApplicationComponent::class)
|
||||
@Module
|
||||
object Provider {
|
||||
|
||||
val mainActivity: MainActivity = MainActivity.getInstance()
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||
|
||||
|
||||
@Provides
|
||||
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
|
||||
return DownloadRecordDatabase.getInstance(appContext).databaseDAO
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getYTDownloader():YoutubeDownloader{
|
||||
return YoutubeDownloader()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUpi():EasyUpiPayment {
|
||||
return EasyUpiPayment.Builder(MainActivity.getInstance())
|
||||
return EasyUpiPayment.Builder(mainActivity)
|
||||
.setPayeeVpa("technoshab@paytm")
|
||||
.setPayeeName("Shabinder Singh")
|
||||
.setTransactionId("UNIQUE_TRANSACTION_ID")
|
||||
@ -72,29 +88,59 @@ object Provider {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getYTDownloader():YoutubeDownloader{
|
||||
return YoutubeDownloader()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
|
||||
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
|
||||
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||
httpClient2.addInterceptor(Interceptor { chain ->
|
||||
.addInterceptor(Interceptor { chain ->
|
||||
val request: Request =
|
||||
chain.request().newBuilder().addHeader(
|
||||
chain.request().newBuilder()
|
||||
.addHeader(
|
||||
"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()
|
||||
chain.proceed(request)
|
||||
})
|
||||
}).addInterceptor(NetworkInterceptor())
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://accounts.spotify.com/")
|
||||
.client(httpClient2.build())
|
||||
.addConverterFactory(MoshiConverterFactory.create(getMoshi()))
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
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()
|
||||
}
|
||||
}
|
286
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
286
app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt
Normal file → Executable file
@ -17,16 +17,288 @@
|
||||
|
||||
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() {
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir)
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/")
|
||||
createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/")
|
||||
createDirectory(defaultDir)
|
||||
createDirectory(defaultDir + ".Images/")
|
||||
createDirectory(defaultDir + "Tracks/")
|
||||
createDirectory(defaultDir + "Albums/")
|
||||
createDirectory(defaultDir + "Playlists/")
|
||||
createDirectory(defaultDir + "YT_Downloads/")
|
||||
}
|
||||
fun getEmojiByUnicode(unicode: Int): String? {
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||
import android.content.BroadcastReceiver
|
||||
@ -28,27 +29,37 @@ import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.arthenica.mobileffmpeg.Config
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
||||
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.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
|
||||
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.fetch2core.DownloadBlock
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class ForegroundService : Service(){
|
||||
private val tag = "Foreground Service"
|
||||
private val channelId = "ForegroundDownloaderService"
|
||||
@ -56,22 +67,21 @@ class ForegroundService : Service(){
|
||||
private var total = 0 //Total Downloads Requested
|
||||
private var converted = 0//Total Files Converted
|
||||
private var downloaded = 0//Total Files downloaded
|
||||
private var fetch:Fetch? = null
|
||||
private var downloadManager : DownloadManager? = null
|
||||
private var downloadList = mutableListOf<DownloadObject>()
|
||||
private lateinit var fetch:Fetch
|
||||
private lateinit var ytDownloader: YoutubeDownloader
|
||||
private lateinit var downloadManager : DownloadManager
|
||||
private var serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
private val requestMap = mutableMapOf<Request,Track>()
|
||||
private val downloadMap = mutableMapOf<String,Track>()
|
||||
private val requestMap = mutableMapOf<Request, TrackDetails>()
|
||||
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(),
|
||||
defaultDirectory+File.separator
|
||||
defaultDir +File.separator
|
||||
)
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var isServiceStarted = false
|
||||
var notificationLine = 0
|
||||
val messageList = mutableListOf<String>("","","","")
|
||||
val messageList = mutableListOf("","","","")
|
||||
private var pendingIntent:PendingIntent? = null
|
||||
|
||||
|
||||
@ -88,7 +98,7 @@ class ForegroundService : Service(){
|
||||
0, notificationIntent, 0
|
||||
)
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
ytDownloader = YoutubeDownloader()
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
@ -97,90 +107,41 @@ class ForegroundService : Service(){
|
||||
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
// fetch?.enableLogging(true)
|
||||
fetch?.addListener(fetchListener)
|
||||
fetch.addListener(fetchListener)
|
||||
//clearing all not completed Downloads
|
||||
//Starting fresh
|
||||
fetch?.removeAll()
|
||||
fetch.removeAll()
|
||||
|
||||
startForeground()
|
||||
}
|
||||
|
||||
/**
|
||||
*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
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
// Send a notification that service is started
|
||||
Log.i(tag,"Service Started.")
|
||||
startForeground()
|
||||
//do heavy work on a background thread
|
||||
//val list = intent.getSerializableExtra("list") as List<Any?>
|
||||
// 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
|
||||
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
|
||||
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
|
||||
|
||||
fetch!!.enqueue(request,
|
||||
{
|
||||
obj.track?.let { it1 -> requestMap.put(it, it1) }
|
||||
downloadList.remove(obj)
|
||||
Log.i(tag, "Enqueuing Download")
|
||||
},
|
||||
{
|
||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
||||
)
|
||||
imagesList?.let{
|
||||
serviceScope.launch {
|
||||
loadAllImages(it)
|
||||
}
|
||||
}
|
||||
|
||||
downloadObjects?.let {
|
||||
total += downloadObjects.size
|
||||
updateNotification()
|
||||
downloadAllTracks(downloadObjects)
|
||||
}
|
||||
|
||||
//Wake locks and misc tasks from here :
|
||||
return if (isServiceStarted){
|
||||
//Service Already Started
|
||||
START_STICKY
|
||||
} else{
|
||||
Log.i(tag,"Starting the foreground service task")
|
||||
isServiceStarted = true
|
||||
|
||||
wakeLock =
|
||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||
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() {
|
||||
super.onDestroy()
|
||||
if(downloadMap.isEmpty() && converted == total){
|
||||
if(converted == total){
|
||||
Handler().postDelayed({
|
||||
Log.i(tag,"Service destroyed.")
|
||||
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?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if(downloadMap.isEmpty() && converted == total ){
|
||||
if(converted == total ){
|
||||
Log.i(tag,"Service Removed.")
|
||||
deleteFile(parentDirectory)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
stopForeground(true)
|
||||
} 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
|
||||
@ -277,23 +249,23 @@ class ForegroundService : Service(){
|
||||
val track = requestMap[download.request]
|
||||
when(notificationLine){
|
||||
0 -> {
|
||||
messageList[0] = "Downloading ${track?.name}"
|
||||
messageList[0] = "Downloading ${track?.title}"
|
||||
notificationLine = 1
|
||||
}
|
||||
1 -> {
|
||||
messageList[1] = "Downloading ${track?.name}"
|
||||
messageList[1] = "Downloading ${track?.title}"
|
||||
notificationLine = 2
|
||||
}
|
||||
2-> {
|
||||
messageList[2] = "Downloading ${track?.name}"
|
||||
messageList[2] = "Downloading ${track?.title}"
|
||||
notificationLine = 3
|
||||
}
|
||||
3 -> {
|
||||
messageList[3] = "Downloading ${track?.name}"
|
||||
messageList[3] = "Downloading ${track?.title}"
|
||||
notificationLine = 0
|
||||
}
|
||||
}
|
||||
Log.i(tag,"${track?.name} Download Started")
|
||||
Log.i(tag,"${track?.title} Download Started")
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
@ -312,32 +284,27 @@ class ForegroundService : Service(){
|
||||
override fun onCompleted(download: Download) {
|
||||
val track = requestMap[download.request]
|
||||
for (message in messageList){
|
||||
if( message == "Downloading ${track?.name}"){
|
||||
if( message == "Downloading ${track?.title}"){
|
||||
//Remove Downloading Status from Notification
|
||||
messageList[messageList.indexOf(message)] = ""
|
||||
}
|
||||
}
|
||||
//Notify Download Completed
|
||||
val intent = Intent()
|
||||
.setAction("track_download_completed")
|
||||
.putExtra("track",track)
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
|
||||
|
||||
serviceScope.launch {
|
||||
try{
|
||||
convertToMp3(download.file, track!!)
|
||||
Log.i(tag,"${track.name} Download Completed")
|
||||
track?.let { convertToMp3(download.file, it) }
|
||||
Log.i(tag,"${track?.title} Download Completed")
|
||||
}catch (e:KotlinNullPointerException
|
||||
){
|
||||
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag,"${track?.name} Requesting Download thru Android DM")
|
||||
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
|
||||
Log.i(tag,"${track?.title} Requesting Download thru Android DM")
|
||||
downloadUsingDM(download.request.url,download.request.file, track!!)
|
||||
downloaded++
|
||||
requestMap.remove(download.request)
|
||||
}
|
||||
}
|
||||
speed = 0
|
||||
updateNotification()
|
||||
// updateNotification()
|
||||
}
|
||||
|
||||
override fun onDeleted(download: Download) {
|
||||
@ -357,7 +324,7 @@ class ForegroundService : Service(){
|
||||
val track = requestMap[download.request]
|
||||
downloaded++
|
||||
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!!)
|
||||
requestMap.remove(download.request)
|
||||
}
|
||||
@ -374,9 +341,9 @@ class ForegroundService : Service(){
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
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)
|
||||
updateNotification()
|
||||
// updateNotification()
|
||||
}
|
||||
|
||||
}
|
||||
@ -384,7 +351,7 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
* 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 request = DownloadManager.Request(uri)
|
||||
.setAllowedNetworkTypes(
|
||||
@ -392,14 +359,14 @@ class ForegroundService : Service(){
|
||||
DownloadManager.Request.NETWORK_MOBILE
|
||||
)
|
||||
.setAllowedOverRoaming(false)
|
||||
.setTitle(track.name)
|
||||
.setTitle(track.title)
|
||||
.setDescription("Spotify Downloader Working Up here...")
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
|
||||
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
|
||||
))
|
||||
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
//Start Download
|
||||
val downloadID = downloadManager?.enqueue(request)
|
||||
val downloadID = downloadManager.enqueue(request)
|
||||
Log.i("DownloadManager", "Download Request Sent")
|
||||
|
||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
@ -421,7 +388,7 @@ class ForegroundService : Service(){
|
||||
/**
|
||||
*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)
|
||||
|
||||
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)
|
||||
mp3File = removeAllTags(mp3File)
|
||||
mp3File = setId3v1Tags(mp3File,track)
|
||||
@ -457,11 +424,17 @@ class ForegroundService : Service(){
|
||||
newFile.renameTo(file)
|
||||
converted++
|
||||
updateNotification()
|
||||
|
||||
//Notify Download Completed
|
||||
val intent = Intent()
|
||||
.setAction("track_download_completed")
|
||||
.putExtra("track",track)
|
||||
this@ForegroundService.sendBroadcast(intent)
|
||||
|
||||
//All tasks completed (REST IN PEACE)
|
||||
if(converted == total){
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -475,7 +448,7 @@ class ForegroundService : Service(){
|
||||
.setSubText("Total: $total Completed:$converted")
|
||||
.setNotificationSilent()
|
||||
.setStyle(NotificationCompat.InboxStyle()
|
||||
.setBigContentTitle("Speed: $speed KB/s")
|
||||
// .setBigContentTitle("Speed: $speed KB/s")
|
||||
.addLine(messageList[0])
|
||||
.addLine(messageList[1])
|
||||
.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 {
|
||||
val id3v1Tag = ID3v1Tag()
|
||||
id3v1Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v1Tag.artist = artistsList.joinToString()
|
||||
id3v1Tag.title = track.name
|
||||
id3v1Tag.album = track.album?.name
|
||||
id3v1Tag.year = track.album?.release_date
|
||||
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
|
||||
val id3v1Tag = ID3v1Tag().apply {
|
||||
artist = track.artists.joinToString(",")
|
||||
title = track.title
|
||||
album = track.albumName
|
||||
year = track.year
|
||||
comment = "Genres:${track.comment}"
|
||||
}
|
||||
mp3File.id3v1Tag = id3v1Tag
|
||||
return mp3File
|
||||
}
|
||||
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
|
||||
val id3v2Tag = ID3v24Tag()
|
||||
id3v2Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v2Tag.artist = artistsList.joinToString()
|
||||
id3v2Tag.title = track.name
|
||||
id3v2Tag.album = track.album?.name
|
||||
id3v2Tag.year = track.album?.release_date
|
||||
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
id3v2Tag.lyrics = "Gonna Implement Soon"
|
||||
val copyrights = mutableListOf<String>()
|
||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
||||
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)
|
||||
private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File {
|
||||
val id3v2Tag = ID3v24Tag().apply {
|
||||
artist = track.artists.joinToString(",")
|
||||
title = track.title
|
||||
album = track.albumName
|
||||
year = track.year
|
||||
comment = "Genres:${track.comment}"
|
||||
lyrics = "Gonna Implement Soon"
|
||||
url = track.trackUrl
|
||||
}
|
||||
val bytesArray = ByteArray(track.albumArt.length().toInt())
|
||||
try{
|
||||
val fis = FileInputStream(track.albumArt)
|
||||
fis.read(bytesArray) //read file into bytes[]
|
||||
fis.close()
|
||||
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
|
||||
return mp3file
|
||||
}
|
||||
|
||||
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
||||
if (mp3file.hasId3v1Tag()) {
|
||||
mp3file.removeId3v1Tag()
|
||||
@ -557,4 +508,146 @@ class ForegroundService : Service(){
|
||||
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/>.
|
||||
-->
|
||||
|
||||
<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
|
||||
android:angle="90"
|
||||
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
|
||||
~ 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
|
||||
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="match_parent">
|
||||
|
||||
@ -27,14 +26,10 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/text_background_accented"
|
||||
style="@style/Widget.AppCompat.TextView.Gradient"
|
||||
android:drawablePadding="5dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:text=" Download History "
|
||||
android:textAlignment="center"
|
||||
android:textColor="#E1FFFFFF"
|
||||
android:textSize="21sp"
|
||||
app:drawableStartCompat="@drawable/ic_history"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@ -43,6 +38,7 @@
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:background="@color/black"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
@ -60,6 +56,11 @@
|
||||
android:icon="@drawable/ic_spotify_logo"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@ -82,4 +83,3 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
@ -17,89 +17,81 @@
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="downloadRecord"
|
||||
type="com.shabinder.spotiflyer.database.DownloadRecord" />
|
||||
</data>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="92dp"
|
||||
android:background="#000000"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="92dp"
|
||||
android:background="#000000"
|
||||
android:paddingBottom="12dp">
|
||||
<ImageView
|
||||
android:id="@+id/coverUrl"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:contentDescription="Track Image"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_song_placeholder" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/coverUrl"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:contentDescription="Track Image"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_song_placeholder" />
|
||||
<TextView
|
||||
android:id="@+id/item_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="14dp"
|
||||
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
|
||||
android:id="@+id/item_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="14dp"
|
||||
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
|
||||
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
|
||||
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
|
||||
android:id="@+id/totalItems"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/coverUrl"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_toStartOf="@+id/btn_action"
|
||||
android:paddingLeft="9dp"
|
||||
android:text="50 Tracks"
|
||||
android:textSize="12sp" />
|
||||
<TextView
|
||||
android:id="@+id/totalItems"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@+id/coverUrl"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:layout_toStartOf="@+id/btn_action"
|
||||
android:paddingLeft="9dp"
|
||||
android:text="50 Tracks"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_action"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_alignBottom="@+id/coverUrl"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_share_open"
|
||||
android:tint="@null" />
|
||||
<ImageButton
|
||||
android:id="@+id/btn_action"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_alignBottom="@+id/coverUrl"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/ic_share_open"
|
||||
android:tint="@null" />
|
||||
|
||||
</RelativeLayout>
|
||||
</layout>
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -16,43 +16,35 @@
|
||||
~ 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: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
|
||||
android:id="@+id/mainActivity"
|
||||
<fragment
|
||||
android:id="@+id/navHostFragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="@drawable/text_background_accented"
|
||||
android:padding="5dp"
|
||||
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" />
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/navigation"
|
||||
tools:ignore="FragmentTagUsage" />
|
||||
|
||||
<fragment
|
||||
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>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -15,46 +15,40 @@
|
||||
~ 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">
|
||||
<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_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
tools:ignore="HardcodedText"
|
||||
>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/linkSearch"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/Widget.AppCompat.TextView.Gradient"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/text_background_accented"
|
||||
android:ems="10"
|
||||
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_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:importantForAutofill="no"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_search"
|
||||
android:layout_width="wrap_content"
|
||||
style="@style/Widget.AppCompat.Button.Colored.Gradient"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/btn_design"
|
||||
android:paddingLeft="4dp"
|
||||
android:paddingRight="4dp"
|
||||
android:text="Search"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp"
|
||||
android:fontFamily="@font/nunito_sans_light"
|
||||
android:text=" Search "
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/linkSearch"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkSearch"
|
||||
@ -74,19 +68,16 @@
|
||||
app:layout_collapseMode="parallax"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkSearch" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkSearch"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_history"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/transparent"
|
||||
android:scaleType="fitCenter"
|
||||
style="@style/Widget.AppCompat.ImageButton.40dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/appLogo"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_history" />
|
||||
android:contentDescription="Open Download History Button"
|
||||
app:srcCompat="@drawable/ic_history"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appName"
|
||||
@ -94,16 +85,13 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:gravity="end"
|
||||
android:text='"SpotiFlyer"'
|
||||
android:textAlignment="viewEnd"
|
||||
android:textColor="#9AB3FF"
|
||||
android:textColor="@color/colorAccent"
|
||||
android:textSize="40sp"
|
||||
android:typeface="normal"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appLogo" />
|
||||
app:layout_constraintTop_toBottomOf="@id/appLogo"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appSubTitle"
|
||||
@ -119,49 +107,36 @@
|
||||
app:layout_constraintStart_toStartOf="@+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
|
||||
android:id="@+id/btn_spotify"
|
||||
android:layout_width="46dp"
|
||||
android:layout_height="46dp"
|
||||
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:background="@color/black"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="Open Spotify App Button"
|
||||
android:src="@drawable/ic_spotify_logo"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/platforms"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_youtube"
|
||||
app:layout_constraintStart_toEndOf="@+id/platforms"
|
||||
app:layout_constraintTop_toTopOf="@+id/platforms" />
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_Gaana"
|
||||
android:padding="6dp"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/appSubTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appSubTitle"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_youtube"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:background="@color/black"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||
android:contentDescription="Open Youtube App Button"
|
||||
android:src="@drawable/ic_youtube"
|
||||
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_constraintTop_toTopOf="@+id/btn_spotify" />
|
||||
|
||||
@ -170,15 +145,15 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="center"
|
||||
android:text="Usage Instructions!"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#D0838383"
|
||||
android:textSize="14sp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/developer_insta_spotify"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/btn_Insta"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
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
|
||||
android:id="@+id/btn_donate"
|
||||
@ -188,6 +163,7 @@
|
||||
android:background="@drawable/text_background_accented"
|
||||
android:drawableEnd="@drawable/ic_mug"
|
||||
android:drawablePadding="5dp"
|
||||
android:contentDescription="Donate Money Button"
|
||||
android:fontFamily="@font/capriola"
|
||||
android:foreground="@drawable/rounded_gradient"
|
||||
android:gravity="end|center_vertical"
|
||||
@ -212,40 +188,31 @@
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_github_spotify"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:id="@+id/btn_github"
|
||||
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||
android:src="@drawable/ic_github"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btn_linkedin"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_youtube"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
android:contentDescription="Open Github App Button"
|
||||
app:layout_constraintVertical_chainStyle="packed"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_linkedin"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||
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_constraintTop_toBottomOf="@+id/btn_github_spotify" />
|
||||
android:contentDescription="Open LinkedIN App Button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_github"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/developer_insta_spotify"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:padding="5dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:id="@+id/btn_Insta"
|
||||
style="@style/Widget.AppCompat.ImageButton.platformIcon"
|
||||
android:src="@drawable/ic_instagram"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btn_donate"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:contentDescription="Open Instagram App Button"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_linkedin" />
|
||||
|
||||
<TextView
|
||||
@ -289,4 +256,3 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
@ -16,19 +16,20 @@
|
||||
~ 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
|
||||
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:background="@color/black"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="25dp"
|
||||
android:fitsSystemWindows="true"
|
||||
android:paddingTop="16dp"
|
||||
tools:context=".ui.spotify.SpotifyFragment">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_download_all_spotify"
|
||||
android:id="@+id/btn_download_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:background="@drawable/btn_design"
|
||||
@ -39,19 +40,18 @@
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp"
|
||||
android:visibility="visible"
|
||||
app:layout_anchor="@+id/appbar_spotify"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/downloading_fab_spotify"
|
||||
android:id="@+id/downloading_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:keepScreenOn="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="fitCenter"
|
||||
android:visibility="gone"
|
||||
app:borderWidth="0dp"
|
||||
app:layout_anchor="@+id/appbar_spotify"
|
||||
app:layout_anchor="@+id/appbar"
|
||||
app:layout_anchorGravity="bottom|center"
|
||||
app:maxImageSize="38dp"
|
||||
app:rippleColor="@color/colorPrimaryDark"
|
||||
@ -59,7 +59,7 @@
|
||||
app:tint="@null" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_spotify"
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="280dp">
|
||||
|
||||
@ -72,14 +72,14 @@
|
||||
app:toolbarId="@+id/toolbar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/TopLayout_spotify"
|
||||
android:id="@+id/topLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@color/black"
|
||||
android:foreground="@drawable/gradient"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/spotify_cover_image"
|
||||
android:id="@+id/cover_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="28dp"
|
||||
@ -89,13 +89,13 @@
|
||||
android:src="@drawable/spotify_download"
|
||||
android:visibility="visible"
|
||||
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_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/StatusBar_spotify"
|
||||
android:id="@+id/statusBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2dp"
|
||||
@ -113,11 +113,11 @@
|
||||
android:textColor="@color/grey"
|
||||
android:textSize="16sp"
|
||||
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_constraintStart_toStartOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/title_view_spotify"
|
||||
android:id="@+id/title_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
@ -142,7 +142,7 @@
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/track_list_spotify"
|
||||
android:id="@+id/track_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="26dp"
|
||||
@ -152,16 +152,7 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" />
|
||||
|
||||
<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"/>
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
@ -16,97 +16,84 @@
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ 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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="track"
|
||||
type="com.shabinder.spotiflyer.models.Track" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
<ImageView
|
||||
android:id="@+id/imageUrl"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="#000000"
|
||||
android:layout_marginBottom="12dp"
|
||||
>
|
||||
android:contentDescription="Track Image"
|
||||
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
|
||||
android:id="@+id/imageUrl"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="Track Image"
|
||||
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" />
|
||||
<TextView
|
||||
android:id="@+id/track_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:letterSpacing="0.04"
|
||||
android:lines="1"
|
||||
android:text="The Spectre"
|
||||
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
|
||||
android:id="@+id/track_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:letterSpacing="0.04"
|
||||
android:lines="1"
|
||||
android:text="The Spectre"
|
||||
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
|
||||
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
|
||||
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
|
||||
android:id="@+id/duration"
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="3dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:text="4 minutes, 20 sec"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/artist"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||
app:layout_constraintStart_toEndOf="@+id/artist"
|
||||
app:layout_constraintTop_toTopOf="@+id/artist" />
|
||||
<TextView
|
||||
android:id="@+id/duration"
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="3dp"
|
||||
android:paddingLeft="9dp"
|
||||
android:text="4 minutes, 20 sec"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/artist"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_download"
|
||||
app:layout_constraintStart_toEndOf="@+id/artist"
|
||||
app:layout_constraintTop_toTopOf="@+id/artist" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_download"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="0dp"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_arrow" />
|
||||
<ImageButton
|
||||
android:id="@+id/btn_download"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="0dp"
|
||||
android:backgroundTint="@color/black"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_arrow" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -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"
|
||||
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
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="com.shabinder.spotiflyer.ui.mainfragment.MainFragment"
|
||||
@ -51,16 +42,11 @@
|
||||
app:destination="@id/downloadRecord"
|
||||
app:enterAnim="@android:anim/slide_in_left"
|
||||
app:exitAnim="@android:anim/slide_out_right" />
|
||||
<action
|
||||
android:id="@+id/action_mainFragment_to_gaanaFragment"
|
||||
app:destination="@id/gaanaFragment" />
|
||||
</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
|
||||
android:id="@+id/downloadRecord"
|
||||
android:name="com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragment"
|
||||
@ -76,5 +62,35 @@
|
||||
app:destination="@id/youtubeFragment"
|
||||
app:enterAnim="@android:anim/slide_in_left"
|
||||
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>
|
||||
</navigation>
|
@ -20,10 +20,12 @@
|
||||
<resources>
|
||||
<color name="colorPrimary">#FC5C7D</color>
|
||||
<color name="colorPrimaryDark">#CE1CFF</color>
|
||||
<color name="colorAccent">#799BFF</color>
|
||||
<color name="colorAccent">#9AB3FF</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="grey">#99FFFFFF</color>
|
||||
<color name="black">#000000</color>
|
||||
|
||||
<color name="dark">#121212</color>
|
||||
<color name="successGreen">#59C351</color>
|
||||
<color name="errorRed">#FF9494</color>
|
||||
|
||||
</resources>
|
@ -19,37 +19,89 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- 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="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="colorAccent">#6A82FB</item>
|
||||
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">#A9B200FF</item>
|
||||
<item name="android:radius">11dp</item>
|
||||
<item name="android:textColorHint">@color/grey</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">@color/colorPrimaryDark</item>
|
||||
<item name="android:radius">12dp</item>
|
||||
<!-- Text Appearances !-->
|
||||
<!-- use our brand's custom TextAppearance4 !-->
|
||||
<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>
|
||||
</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">
|
||||
<item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item>
|
||||
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
|
||||
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
|
||||
<item name="android:textSize">22sp</item>
|
||||
<item name="fontFamily">@font/amita</item>
|
||||
</style>
|
||||
<style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent">
|
||||
<item name="background">@color/white</item>
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">20dp</item>
|
||||
<item name="cornerSize">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton">
|
||||
<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:textSize">14sp</item>
|
||||
<item name="android:layout_marginEnd">4dp</item>
|
||||
<item name="android:layout_marginBottom">2dp</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
|
@ -18,8 +18,8 @@
|
||||
|
||||
<AppUpdater>
|
||||
<update>
|
||||
<latestVersion>1.5.1</latestVersion>
|
||||
<latestVersionCode>7</latestVersionCode>
|
||||
<url>https://github.com/Shabinder/SpotiFlyer/releases/download/1.5/SpotiFlyer-v1.5.apk</url>
|
||||
<latestVersion>1.6</latestVersion>
|
||||
<latestVersionCode>8</latestVersionCode>
|
||||
<url>https://github.com/Shabinder/SpotiFlyer/releases/</url>
|
||||
</update>
|
||||
</AppUpdater>
|
@ -20,7 +20,7 @@ buildscript {
|
||||
ext{
|
||||
kotlin_version = "1.4.10"
|
||||
navigationVersion = '2.3.0'
|
||||
ext.hilt_version = '2.28-alpha'
|
||||
ext.hilt_version = '2.29.1-alpha'
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@ -33,7 +33,7 @@ buildscript {
|
||||
//safe-Args
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
||||
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
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user