Merge pull request #6 from Shabinder/develop

Develop Branch Merge For v1.6
This commit is contained in:
Shabinder Singh 2020-11-10 17:41:08 +05:30 committed by GitHub
commit 1f11f2bf9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 3238 additions and 2088 deletions

View File

@ -1,14 +1,22 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="shabinder"> <dictionary name="shabinder">
<words> <words>
<w>albumseokey</w>
<w>amita</w>
<w>cardview</w>
<w>cherrypick</w>
<w>downloadrecord</w> <w>downloadrecord</w>
<w>emoji</w> <w>emoji</w>
<w>ffmpeg</w> <w>ffmpeg</w>
<w>flyer</w> <w>flyer</w>
<w>gaana</w>
<w>gener</w>
<w>hqdefault</w>
<w>insta</w> <w>insta</w>
<w>instagram</w> <w>instagram</w>
<w>jetbrains</w> <w>jetbrains</w>
<w>kotlinx</w> <w>kotlinx</w>
<w>linkedin</w>
<w>mainfragment</w> <w>mainfragment</w>
<w>maxresdefault</w> <w>maxresdefault</w>
<w>moshi</w> <w>moshi</w>
@ -17,14 +25,17 @@
<w>musicplaceholder</w> <w>musicplaceholder</w>
<w>raleway</w> <w>raleway</w>
<w>semibold</w> <w>semibold</w>
<w>seokey</w>
<w>shabinder</w> <w>shabinder</w>
<w>singh</w> <w>singh</w>
<w>snackbar</w>
<w>spoti</w> <w>spoti</w>
<w>spotiflyer</w> <w>spotiflyer</w>
<w>spotify</w> <w>spotify</w>
<w>spotifydownloader</w> <w>spotifydownloader</w>
<w>spotifyler</w> <w>spotifyler</w>
<w>thru</w> <w>thru</w>
<w>weyfdnx</w>
<w>youtu</w> <w>youtu</w>
</words> </words>
</dictionary> </dictionary>

View File

@ -21,22 +21,23 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin" apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'dagger.hilt.android.plugin'
//apply plugin: 'kotlinx-serialization' apply plugin: 'kotlinx-serialization'
android { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion "30.0.2" buildToolsVersion "30.0.2"
buildFeatures{ buildFeatures{
dataBinding = true //dataBinding = true
viewBinding = true
} }
defaultConfig { defaultConfig {
applicationId 'com.shabinder.spotiflyer' applicationId 'com.shabinder.spotiflyer'
minSdkVersion 22 minSdkVersion 22
targetSdkVersion 29 targetSdkVersion 29
versionCode 7 versionCode 8
versionName "1.5.1" versionName "1.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
packagingOptions { packagingOptions {
@ -90,8 +91,10 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
implementation "androidx.room:room-runtime:2.2.5" implementation "androidx.room:room-runtime:2.2.5"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
kapt "androidx.room:room-compiler:2.2.5" kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-ktx:2.2.5" implementation "androidx.room:room-ktx:2.2.5"
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
@ -115,10 +118,13 @@ dependencies {
implementation 'com.squareup.moshi:moshi:1.11.0' implementation 'com.squareup.moshi:moshi:1.11.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0' implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
implementation "com.squareup.retrofit2:converter-moshi:2.9.0" implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.beust:klaxon:5.4'
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0' implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.2' implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5" implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
implementation 'com.github.javiersantos:AppUpdater:2.7' implementation 'com.github.javiersantos:AppUpdater:2.7'

View File

@ -11,7 +11,25 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *; # public *;
#} #}
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.* {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.* {
kotlinx.serialization.KSerializer serializer(...);
}
# Change here com.yourcompany.yourpackage
-keep,includedescriptorclasses class com.shabinder.spotiflyer.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class com.shabinder.spotiflyer* { # <-- change package name to your app's
*** Companion;
}
-keepclasseswithmembers class com.shabinder.spotiflyer.* { # <-- change package name to your app's
kotlinx.serialization.KSerializer serializer(...);
}
# Uncomment this to preserve the line number information for # Uncomment this to preserve the line number information for
# debugging stack traces. # debugging stack traces.
#-keepattributes SourceFile,LineNumberTable #-keepattributes SourceFile,LineNumberTable

View File

@ -21,24 +21,24 @@ import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.findNavController
import com.github.javiersantos.appupdater.AppUpdater import com.github.javiersantos.appupdater.AppUpdater
import com.github.javiersantos.appupdater.enums.UpdateFrom import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.databinding.MainActivityBinding
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.createDirectories
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -49,49 +49,48 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import javax.inject.Inject import javax.inject.Inject
/*
* This is App's God Activity
* */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(){ class MainActivity : AppCompatActivity(){
private var spotifyService : SpotifyService? = null private var spotifyService : SpotifyService? = null
private var isConnected: Boolean = false
private var sharedPref :SharedPreferences? = null
private var token :String =""
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
lateinit var snackBarAnchor: View
private lateinit var sharedViewModel: SharedViewModel private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest private lateinit var navController: NavController
@Inject lateinit var moshi: Moshi @Inject lateinit var moshi: Moshi
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
//Enabling Dark Mode //Enabling Dark Mode
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
sharedPref = this.getPreferences(Context.MODE_PRIVATE) binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
navController = findNavController(R.id.navHostFragment)
snackBarAnchor = binding.snackBarPosition
//starting Notification and Downloader Service!
SpotifyDownloadHelper.startService(this)
if(sharedViewModel.spotifyService.value == null){
authenticateSpotify() authenticateSpotify()
}else{
implementSpotifyService(sharedViewModel.accessToken.value!!)
}
requestPermission() requestPermission()
disableDozeMode() disableDozeMode()
checkIfLatestVersion() checkIfLatestVersion()
createDirectories() createDirectories()
isConnected = sharedViewModel.isOnline(this) Log.i("Connection Status", isOnline().toString())
sharedViewModel.isConnected.value = isConnected
Log.i("Connection Status", isConnected.toString()) //starting Notification and Downloader Service!
startService(this)
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
Log.i("NEW INTENT", "Received") //Return to MainFragment For Further Processing of this Intent
navController.popBackStack(R.id.mainFragment,false)
handleIntentFromExternalActivity(intent) handleIntentFromExternalActivity(intent)
} }
@ -102,9 +101,10 @@ class MainActivity : AppCompatActivity(){
this.getSystemService(Context.POWER_SERVICE) as PowerManager this.getSystemService(Context.POWER_SERVICE) as PowerManager
val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName) val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName)
if (!isIgnoringBatteryOptimizations) { if (!isIgnoringBatteryOptimizations) {
val intent = Intent() val intent = Intent().apply{
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:$packageName") data = Uri.parse("package:$packageName")
}
startActivityForResult(intent, 1233) startActivityForResult(intent, 1233)
} }
} }
@ -139,7 +139,7 @@ class MainActivity : AppCompatActivity(){
"Bearer $token" "Bearer $token"
).build() ).build()
chain.proceed(request) chain.proceed(request)
}) }).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://api.spotify.com/v1/") .baseUrl("https://api.spotify.com/v1/")
@ -154,16 +154,13 @@ class MainActivity : AppCompatActivity(){
fun authenticateSpotify() { fun authenticateSpotify() {
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
if (isConnected) { Log.i("Spotify Authentication","Started")
Log.i("Post Request", "Made") val token = spotifyServiceTokenRequest.getToken()
token = spotifyServiceTokenRequest.getToken()!!.access_token token.value?.let {
implementSpotifyService(token) showMessage("Success: Spotify Token Acquired",isSuccess = true)
Log.i("Post Request", token) implementSpotifyService(it.access_token)
sharedViewModel.accessToken.value = token
}else{
Log.i("network", "unavailable")
// sharedViewModel.showAlertDialog(resources,this@MainActivity)
} }
Log.i("Spotify Token", token.value.toString())
} }
} }
@ -188,25 +185,14 @@ class MainActivity : AppCompatActivity(){
} }
} }
override fun onSaveInstanceState(savedInstanceState: Bundle) {
savedInstanceState.putString("token", token)
super.onSaveInstanceState(savedInstanceState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
if (savedInstanceState.getString("token") ==""){
super.onRestoreInstanceState(savedInstanceState)
}else{
implementSpotifyService(savedInstanceState.getString("token")!!)
super.onRestoreInstanceState(savedInstanceState)
}
}
private fun checkIfLatestVersion() { private fun checkIfLatestVersion() {
val appUpdater = AppUpdater(this) val appUpdater = AppUpdater(this)
.showAppUpdated(false)//true:Show App is Update Dialog .showAppUpdated(false)//true:Show App is Update Dialog
.setUpdateFrom(UpdateFrom.XML) .setUpdateFrom(UpdateFrom.XML)
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml") .setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
.setCancelable(false) .setCancelable(false)
.setButtonDoNotShowAgain("Remind Later")
.setButtonDoNotShowAgainClickListener { dialog, _ -> dialog.dismiss() }
.setButtonUpdateClickListener { _, _ -> .setButtonUpdateClickListener { _, _ ->
val uri: Uri = val uri: Uri =
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases") Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
@ -220,11 +206,10 @@ class MainActivity : AppCompatActivity(){
} }
companion object{ companion object{
private var instance = MainActivity() private lateinit var instance: MainActivity
fun getInstance():MainActivity{ fun getInstance():MainActivity = instance
return instance
}
} }
init { init {
instance = this instance = this
} }

View File

@ -17,49 +17,22 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import android.content.Context
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Environment
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.SpotifyService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.io.File
class SharedViewModel : ViewModel() { class SharedViewModel : ViewModel() {
var intentString = MutableLiveData<String>().apply { value = "" } var intentString = MutableLiveData<String>()
var spotifyService = MutableLiveData<SpotifyService>() var spotifyService = MutableLiveData<SpotifyService>()
var accessToken = MutableLiveData<String>().apply { value = "" }
var isConnected = MutableLiveData<Boolean>().apply { value = false }
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator
private var viewModelJob = Job() private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
viewModelJob.cancel() viewModelJob.cancel()
} }
fun showAlertDialog(resources:Resources,context: Context){
MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text))
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
// Respond to neutral button press
}
.show()
}
fun isOnline(context: Context): Boolean {
val cm =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val netInfo = cm.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
}
} }

View 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"
}
}

View File

@ -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
)

View File

@ -17,49 +17,54 @@
package com.shabinder.spotiflyer.downloadHelper package com.shabinder.spotiflyer.downloadHelper
import android.content.Context
import android.content.Intent
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.view.View import android.widget.Toast
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.github.kiulian.downloader.model.formats.Format
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.worker.ForegroundService import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.removeIllegalChars
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
import com.shabinder.spotiflyer.utils.startService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
object YTDownloadHelper { object YTDownloadHelper {
var context : Context? = null suspend fun downloadYTTracks(
var statusBar: TextView? = null type:String,
subFolder: String?,
fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) { tracks:List<TrackDetails>,
format?.let { ){
val url:String = format.url() val downloadList = ArrayList<DownloadObject>()
// Log.i("DHelper Link Found", url) tracks.forEach {
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + if(!isOnline()){
SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars( showNoConnectionAlert()
ytTrack.name!! return
) +".m4a") }
val outputFile: String =
Environment.getExternalStorageDirectory().toString() + File.separator +
defaultDir +
removeIllegalChars(type) + File.separator +
(if (subFolder == null) { "" }
else { removeIllegalChars(subFolder) + File.separator }
+ removeIllegalChars(it.title) + ".m4a")
val downloadObject = DownloadObject( val downloadObject = DownloadObject(
track = ytTrack, trackDetails = it,
url = url, ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
outputDir = outputFile .substringBeforeLast("."),
outputFile = outputFile
) )
Log.i("DH",outputFile)
startService(context!!, downloadObject) downloadList.add(downloadObject)
statusBar?.visibility= View.VISIBLE }
Log.i("YT Downloader Helper","Download Request Sent")
withContext(Dispatchers.Main){
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
startService(mainActivity,downloadList)
} }
} }
private fun startService(context:Context, obj: DownloadObject? = null ) {
val serviceIntent = Intent(context, ForegroundService::class.java)
serviceIntent.putExtra("object",obj)
ContextCompat.startForegroundService(context, serviceIntent)
}
} }

View File

@ -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()
}

View File

@ -18,12 +18,35 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable import android.os.Parcelable
import com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.io.File
@Parcelize @Parcelize
data class DownloadObject( data class DownloadObject(
var ytVideo: YTTrack?=null, var trackDetails: TrackDetails,
var track: Track?=null, var ytVideoId:String,
var url:String, var outputFile:String
var outputDir:String
):Parcelable ):Parcelable
@Parcelize
data class TrackDetails(
var title:String,
var artists:List<String>,
var durationSec:Int,
var albumName:String?=null,
var year:String?=null,
var comment:String?=null,
var lyrics:String?=null,
var trackUrl:String?=null,
var albumArt: File,
var albumArtURL: String,
var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
):Parcelable
enum class DownloadStatus{
Downloaded,
Downloading,
NotDownloaded
}

View File

@ -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?)

View File

@ -21,11 +21,10 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
data class YTTrack( data class YoutubeTrack(
var id:String?, var name: String? = null,
var title:String?, var type: String? = null, // Song / Video
var duration:Int?, var artist: String? = null,
var author:String?, var duration:String? = null,
var viewCount:Long?, var videoId: String? = null
var thumbnails:List<String?>?
):Parcelable ):Parcelable

View 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/>.
*/
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?
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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>
)

View 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/>.
*/
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>,
)

View File

@ -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>
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.Json import com.squareup.moshi.Json

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -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,
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import com.shabinder.spotiflyer.models.DownloadStatus
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@ -31,7 +32,6 @@ data class Track(
var explicit: Boolean? = null, var explicit: Boolean? = null,
var external_urls: Map<String?, String?>? = null, var external_urls: Map<String?, String?>? = null,
var href: String? = null, var href: String? = null,
var id: String? = null,
var name: String? = null, var name: String? = null,
var preview_url: String? = null, var preview_url: String? = null,
var track_number: Int = 0, var track_number: Int = 0,
@ -40,5 +40,6 @@ data class Track(
var album: Album? = null, var album: Album? = null,
var external_ids: Map<String?, String?>? = null, var external_ids: Map<String?, String?>? = null,
var popularity: Int? = null, var popularity: Int? = null,
var ytCoverUrl:String? = null, var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
var downloaded:String? = "notDownloaded"):Parcelable ):Parcelable

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize

View File

@ -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>
}

View File

@ -15,58 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.networking
import com.shabinder.spotiflyer.models.* import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.spotify.*
import retrofit2.http.* import retrofit2.http.*
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
interface SpotifyService { interface SpotifyService {
@GET("playlists/{playlist_id}") @GET("playlists/{playlist_id}")
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Playlist suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Optional<Playlist>
@GET("playlists/{playlist_id}/tracks") @GET("playlists/{playlist_id}/tracks")
suspend fun getPlaylistTracks( suspend fun getPlaylistTracks(
@Path("playlist_id") playlistId: String?, @Path("playlist_id") playlistId: String?,
@Query("offset") offset: Int = 0, @Query("offset") offset: Int = 0,
@Query("limit") limit: Int = 100 @Query("limit") limit: Int = 100
): PagingObjectPlaylistTrack ): Optional<PagingObjectPlaylistTrack>
@GET("tracks/{id}") @GET("tracks/{id}")
suspend fun getTrack(@Path("id") trackId: String?): Track suspend fun getTrack(@Path("id") trackId: String?): Optional<Track>
@GET("episodes/{id}") @GET("episodes/{id}")
suspend fun getEpisode(@Path("id") episodeId: String?): Track suspend fun getEpisode(@Path("id") episodeId: String?): Optional<Track>
@GET("shows/{id}") @GET("shows/{id}")
suspend fun getShow(@Path("id") showId: String?): Track suspend fun getShow(@Path("id") showId: String?): Optional<Track>
@GET("albums/{id}") @GET("albums/{id}")
suspend fun getAlbum(@Path("id") albumId: String?): Album suspend fun getAlbum(@Path("id") albumId: String?): Optional<Album>
} }
interface SpotifyServiceTokenRequest{ interface SpotifyServiceTokenRequest{
@POST("api/token") @POST("api/token")
@FormUrlEncoded @FormUrlEncoded
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"):Token? suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"): Optional<Token>
} }

View File

@ -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
}

View File

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections
import com.shabinder.spotiflyer.utils.bindImage import com.shabinder.spotiflyer.utils.bindImage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -34,6 +35,8 @@ import kotlinx.coroutines.launch
class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.ViewHolder>(DownloadRecordDiffCallback()) { class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.ViewHolder>(DownloadRecordDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default) private val adapterScope = CoroutineScope(Dispatchers.Default)
//Remember To change when Submitting a Different List / Or Use New Submit List Function
var source:Source = Source.Spotify
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
@ -44,20 +47,31 @@ class DownloadRecordAdapter: ListAdapter<DownloadRecord,DownloadRecordAdapter.Vi
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
adapterScope.launch { adapterScope.launch {
bindImage(holder.binding.coverUrl,item.coverUrl) bindImage(holder.binding.coverUrl,item.coverUrl,source)
} }
holder.binding.itemName.text = item.name holder.binding.itemName.text = item.name
holder.binding.totalItems.text = "Tracks: ${item.totalFiles}" holder.binding.totalItems.text = "Tracks: ${item.totalFiles}"
holder.binding.type.text = item.type holder.binding.type.text = item.type
holder.binding.btnAction.setOnClickListener { holder.binding.btnAction.setOnClickListener {
if (item.link.contains("spotify",true)){ when {
item.link.contains("spotify",true) -> {
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link))) it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link)))
}else if(item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) ){ }
item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) -> {
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link)) it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link))
} }
item.link.contains("gaana",true) -> {
it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToGaanaFragment((item.link)))
}
}
} }
} }
class ViewHolder(val binding: DownloadRecordItemBinding) : RecyclerView.ViewHolder(binding.root) class ViewHolder(val binding: DownloadRecordItemBinding) : RecyclerView.ViewHolder(binding.root)
fun submitList(list: MutableList<DownloadRecord>?,source: Source) {
super.submitList(list)
this.source = source
}
} }
class DownloadRecordDiffCallback: DiffUtil.ItemCallback<DownloadRecord>(){ class DownloadRecordDiffCallback: DiffUtil.ItemCallback<DownloadRecord>(){

View File

@ -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
}
}

View 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
}
}

View File

@ -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
}
}

View File

@ -1,5 +1,5 @@
D/Retrofit: <--- HTTP 200 https://api.spotify.com/v1/me/top/artists (7170ms) D/Retrofit: <--- HTTP 200 https://api.spotify.com/v1/me/top/artists (7170ms)
2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.Artist@4fae9ec, kaaes.spotify.webapi.android.models.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.Artist@ed6004a, kaaes.spotify.webapi.android.models.Artist@870dbbb, kaaes.spotify.webapi.android.models.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.Artist@aab431, kaaes.spotify.webapi.android.models.Artist@a7bd716, kaaes.spotify.webapi.android.models.Artist@3477897, kaaes.spotify.webapi.android.models.Artist@7f68a84] 2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@4fae9ec, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@ed6004a, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@870dbbb, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aab431, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@a7bd716, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@3477897, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@7f68a84]
I/Network: https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN I/Network: https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN

View File

@ -21,12 +21,11 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -41,43 +40,49 @@ class DownloadRecordFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.download_record_fragment,container,false) binding = DownloadRecordFragmentBinding.inflate(inflater,container,false)
downloadRecordViewModel = ViewModelProvider(this).get(DownloadRecordViewModel::class.java) downloadRecordViewModel = ViewModelProvider(this).get(DownloadRecordViewModel::class.java)
adapter = DownloadRecordAdapter() adapter = DownloadRecordAdapter()
binding.downloadRecordList.adapter = adapter binding.downloadRecordList.adapter = adapter
downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, { downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, {
if(it.isNotEmpty()){ if(it.isNotEmpty()){
downloadRecordViewModel.spotifyList = mutableListOf() resetLists()
downloadRecordViewModel.ytList = mutableListOf()
for (downloadRecord in it) { for (downloadRecord in it) {
if(downloadRecord.link.contains("spotify",true)) downloadRecordViewModel.spotifyList.add(downloadRecord) when{
else downloadRecordViewModel.ytList.add(downloadRecord) downloadRecord.link.contains("spotify",true) -> downloadRecordViewModel.spotifyList.add(downloadRecord)
downloadRecord.link.contains("gaana",true) -> downloadRecordViewModel.gaanaList.add(downloadRecord)
else -> downloadRecordViewModel.ytList.add(downloadRecord)
}
}
when(binding.tabLayout.selectedTabPosition){
0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify)
1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana)
2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube)
} }
if(binding.tabLayout.selectedTabPosition == 0) adapter.submitList(downloadRecordViewModel.spotifyList)
else adapter.submitList(downloadRecordViewModel.ytList)
} }
}) })
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
if(tab?.text == "Spotify"){ when(tab?.position){
adapter.submitList(downloadRecordViewModel.spotifyList) 0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify)
} else adapter.submitList(downloadRecordViewModel.ytList) 1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana)
2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube)
} }
override fun onTabReselected(tab: TabLayout.Tab?) {
// Handle tab reselect
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
// Handle tab unselected
} }
override fun onTabReselected(tab: TabLayout.Tab?) {}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
}) })
return binding.root return binding.root
} }
private fun resetLists() {
downloadRecordViewModel.spotifyList = mutableListOf()
downloadRecordViewModel.ytList = mutableListOf()
downloadRecordViewModel.gaanaList = mutableListOf()
}
} }

View File

@ -32,6 +32,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data
private var viewModelJob = Job() private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob) private val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob)
var spotifyList = mutableListOf<DownloadRecord>() var spotifyList = mutableListOf<DownloadRecord>()
var gaanaList = mutableListOf<DownloadRecord>()
var ytList = mutableListOf<DownloadRecord>() var ytList = mutableListOf<DownloadRecord>()
val downloadRecordList = MutableLiveData<MutableList<DownloadRecord>>().apply { val downloadRecordList = MutableLiveData<MutableList<DownloadRecord>>().apply {
value = mutableListOf() value = mutableListOf()
@ -40,6 +41,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data
init { init {
getDownloadRecordList() getDownloadRecordList()
} }
private fun getDownloadRecordList() { private fun getDownloadRecordList() {
uiScope.launch { uiScope.launch {
downloadRecordList.postValue(databaseDAO.getRecord().toMutableList()) downloadRecordList.postValue(databaseDAO.getRecord().toMutableList())

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -17,22 +17,19 @@
package com.shabinder.spotiflyer.ui.mainfragment package com.shabinder.spotiflyer.ui.mainfragment
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.MainFragmentBinding import com.shabinder.spotiflyer.databinding.MainFragmentBinding
import com.shabinder.spotiflyer.utils.*
import com.shreyaspatil.easyupipayment.EasyUpiPayment import com.shreyaspatil.easyupipayment.EasyUpiPayment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -53,123 +50,88 @@ class MainFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false) binding = MainFragmentBinding.inflate(inflater,container,false)
initializeAll() initializeAll()
binding.btnSearch.setOnClickListener { binding.btnSearch.setOnClickListener {
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
val link = binding.linkSearch.text.toString() val link = binding.linkSearch.text.toString()
if (link.contains("spotify",true)){ when{
//SPOTIFY
link.contains("spotify",true) -> {
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
(activity as MainActivity).authenticateSpotify()
}
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link)) findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link))
}else if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){ }
//YOUTUBE
link.contains("youtube.com",true) || link.contains("youtu.be",true) -> {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link)) findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
}else{Toast.makeText(context,"Link is Not Valid",Toast.LENGTH_SHORT).show()} }
//GAANA
link.contains("gaana",true) -> {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToGaanaFragment(link))
}
else -> showMessage("Link is Not Valid",true)
}
} }
handleIntent() handleIntent()
return binding.root return binding.root
} }
private fun initializeAll() {
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
openYTButton()
openSpotifyButton()
openGithubButton()
openInstaButton()
openLinkedInButton()
historyButton()
binding.usage.text = usageText()
binding.btnDonate.setOnClickListener {
easyUpiPayment.startPayment()
}
}
private fun historyButton() {
binding.btnHistory.setOnClickListener {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
}
}
/** /**
* Handle Intent If there is any! * Handle Intent If there is any!
**/ **/
private fun handleIntent() { private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{ sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
if(it != ""){
sharedViewModel.uiScope.launch(Dispatchers.IO) { sharedViewModel.uiScope.launch(Dispatchers.IO) {
while (sharedViewModel.accessToken.value == "") { //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 //Waiting for Authentication to Finish
Thread.sleep(1000) Thread.sleep(1000)
} }
}
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
binding.linkSearch.setText(sharedViewModel.intentString.value) binding.linkSearch.setText(sharedViewModel.intentString.value)
binding.btnSearch.performClick() binding.btnSearch.performClick()
sharedViewModel.intentString.value = "" //Intent Consumed
sharedViewModel.intentString.value = null
} }
} }
} }
}) })
} }
/** private fun initializeAll() {
* Implementing buttons mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
**/ sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
private fun openSpotifyButton() { binding.apply {
val manager: PackageManager = requireActivity().packageManager btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
try { btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
val i = manager.getLaunchIntentForPackage("com.spotify.music") btnYoutube.openPlatformOnClick("com.google.android.youtube","http://m.youtube.com")
?: throw PackageManager.NameNotFoundException() btnGithub.openPlatformOnClick("http://github.com/Shabinder/SpotiFlyer")
i.addCategory(Intent.CATEGORY_LAUNCHER) btnInsta.openPlatformOnClick("http://www.instagram.com/mr.shabinder")
binding.btnSpotify.setOnClickListener { startActivity(i) } btnHistory.setOnClickListener {
} catch (e: PackageManager.NameNotFoundException) { findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
val uri: Uri = }
Uri.parse("http://open.spotify.com") usage.text = usageText()
val intent = Intent(Intent.ACTION_VIEW, uri) btnDonate.setOnClickListener {
binding.btnSpotify.setOnClickListener { easyUpiPayment.startPayment()
startActivity(intent)
} }
} }
} }
private fun openYTButton() {
val manager: PackageManager = requireActivity().packageManager
try {
val i = manager.getLaunchIntentForPackage("com.google.android.youtube")
?: throw PackageManager.NameNotFoundException()
i.addCategory(Intent.CATEGORY_LAUNCHER)
binding.btnYoutube.setOnClickListener { startActivity(i) }
} catch (e: PackageManager.NameNotFoundException) {
val uri: Uri =
Uri.parse("http://m.youtube.com")
val intent = Intent(Intent.ACTION_VIEW, uri)
binding.btnYoutube.setOnClickListener {
startActivity(intent)
}
}
}
private fun openGithubButton() {
val uri: Uri =
Uri.parse("http://github.com/Shabinder/SpotiFlyer")
val intent = Intent(Intent.ACTION_VIEW, uri)
binding.btnGithubSpotify.setOnClickListener {
startActivity(intent)
}
}
private fun openLinkedInButton() {
val uri: Uri =
Uri.parse("https://in.linkedin.com/in/shabinder")
val intent = Intent(Intent.ACTION_VIEW, uri)
binding.btnLinkedin.setOnClickListener {
startActivity(intent)
}
}
private fun openInstaButton() {
val uri: Uri =
Uri.parse("http://www.instagram.com/mr.shabinder")
val intent = Intent(Intent.ACTION_VIEW, uri)
binding.developerInstaSpotify.setOnClickListener {
startActivity(intent)
}
}
private fun usageText(): SpannableStringBuilder { private fun usageText(): SpannableStringBuilder {
return SpannableStringBuilder() return SpannableStringBuilder()
.append(getText(R.string.d_one)).append("\n") .append(getText(R.string.d_one)).append("\n")

View File

@ -18,307 +18,124 @@
package com.shabinder.spotiflyer.ui.spotify package com.shabinder.spotiflyer.ui.spotify
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.WebView
import android.widget.Toast
import androidx.core.net.toUri
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.bumptech.glide.Glide import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.bumptech.glide.load.DataSource import com.shabinder.spotiflyer.models.DownloadStatus
import com.bumptech.glide.load.engine.GlideException import com.shabinder.spotiflyer.models.spotify.Source
import com.bumptech.glide.request.RequestListener import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.bumptech.glide.request.target.Target import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.github.kiulian.downloader.YoutubeDownloader import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
import com.shabinder.spotiflyer.models.Track
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
import com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.copyTo
import com.shabinder.spotiflyer.utils.rotateAnim
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@Suppress("DEPRECATION")
@AndroidEntryPoint @AndroidEntryPoint
class SpotifyFragment : Fragment() { class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() {
private lateinit var binding:SpotifyFragmentBinding
private lateinit var spotifyViewModel: SpotifyViewModel
private lateinit var sharedViewModel: SharedViewModel
private lateinit var adapterSpotify:SpotifyTrackListAdapter
@Inject lateinit var ytDownloader:YoutubeDownloader
private var webView: WebView? = null
private var intentFilter:IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
override lateinit var viewModel: SpotifyViewModel
override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Spotify
override val args: SpotifyFragmentArgs by navArgs()
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = DataBindingUtil.inflate(inflater,R.layout.spotify_fragment,container,false) super.onCreateView(inflater, container, savedInstanceState)
adapterSpotify = SpotifyTrackListAdapter()
initializeAll() initializeAll()
initializeLiveDataObservers()
initializeBroadcast()
val args = SpotifyFragmentArgs.fromBundle(requireArguments()) val spotifyLink = args.link.substringAfter("open.spotify.com/")
val spotifyLink = args.link
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
Log.i("Fragment", "$type : $link") Log.i("Spotify Fragment", "$type : $link")
if(sharedViewModel.spotifyService.value == null){//Authentication pending!! if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
(activity as MainActivity).authenticateSpotify() if(isOnline()) mainActivity.authenticateSpotify()
} }
if(!isOnline()){//Device Offline
sharedViewModel.showAlertDialog(resources,requireContext()) when{
}else if (type == "Error" || link == "Error") {//Incorrect Link type == "Error" || link == "Error" -> {
showToast("Please Check Your Link!") showMessage("Please Check Your Link!")
}else if(spotifyLink.contains("open.spotify",true)){//Link Validation!! mainActivity.onBackPressed()
}
else -> {
if(type == "episode" || type == "show"){//TODO Implementation if(type == "episode" || type == "show"){//TODO Implementation
showToast("Implementing Soon, Stay Tuned!") showMessage("Implementing Soon, Stay Tuned!")
} }
else{ else{
spotifyViewModel.spotifySearch(type,link) this.viewModel.spotifySearch(type,link)
if(type=="album")adapterSpotify.isAlbum = true
binding.btnDownloadAllSpotify.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
for (track in spotifyViewModel.trackList.value!!){ if(!isOnline()){
if(track.downloaded != "Downloaded"){ showNoConnectionAlert()
track.downloaded = "Downloading" return@setOnClickListener
} }
} binding.btnDownloadAll.visibility = View.GONE
binding.btnDownloadAllSpotify.visibility = View.GONE binding.downloadingFab.visibility = View.VISIBLE
binding.downloadingFabSpotify.visibility = View.VISIBLE
rotateAnim(binding.downloadingFab)
rotateAnim(binding.downloadingFabSpotify) for (track in this.viewModel.trackList.value ?: listOf()){
for (track in spotifyViewModel.trackList.value!!){ if(track.downloaded != DownloadStatus.Downloaded){
if(track.downloaded != "Downloaded"){ track.downloaded = DownloadStatus.Downloading
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track)) adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
} }
} }
showToast("Starting Download in Few Seconds") showMessage("Processing!")
spotifyViewModel.uiScope.launch(Dispatchers.Default){loadAllImages(spotifyViewModel.trackList.value!!)} sharedViewModel.uiScope.launch(Dispatchers.Default){
spotifyViewModel.uiScope.launch { val urlList = arrayListOf<String>()
SpotifyDownloadHelper.downloadAllTracks( this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
spotifyViewModel.folderType, //Appending Source
spotifyViewModel.subFolder, urlList.add("spotify")
spotifyViewModel.trackList.value!!, loadAllImages(
ytDownloader 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(),
) )
} }
} }
} }
} }
}
return binding.root return binding.root
} }
override fun onResume() {
super.onResume()
initializeBroadcast()
}
private fun initializeBroadcast() {
intentFilter = IntentFilter()
intentFilter?.addAction("track_download_completed")
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
val track = intent.getParcelableExtra<Track?>("track")
track?.let {
val position: Int = spotifyViewModel.trackList.value?.indexOf(track)!!
Log.i("Track","Download Completed Intent :$position")
track.downloaded = "Downloaded"
if(position != -1) {
spotifyViewModel.trackList.value?.set(position, track)
adapterSpotify.notifyItemChanged(position)
checkIfAllDownloaded()
}
}
}
}
}
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(updateUIReceiver)
}
/**
*Live Data Observers
**/
private fun initializeLiveDataObservers() {
/**
* CoverUrl Binding Observer!
**/
spotifyViewModel.coverUrl.observe(viewLifecycleOwner, {
if(it!="Loading") bindImage(binding.spotifyCoverImage,it)
})
/**
* TrackList Binding Observer!
**/
spotifyViewModel.trackList.observe(viewLifecycleOwner, {
if (it.isNotEmpty()){
Log.i("SpotifyFragment","TrackList Updated")
adapterConfig(it)
checkIfAllDownloaded()
}
})
/**
* Title Binding Observer!
**/
spotifyViewModel.title.observe(viewLifecycleOwner, {
binding.titleViewSpotify.text = it
})
sharedViewModel.intentString.observe(viewLifecycleOwner,{
//Waiting for Authentication to Finish with Spotify()Access Token Observe
if(it != "" && it!=SpotifyFragmentArgs.fromBundle(requireArguments()).link){
//New Intent Received , Time TO RELOAD
(activity as MainActivity).onBackPressed()
}
})
}
private fun checkIfAllDownloaded() {
if(!spotifyViewModel.trackList.value!!.any { it.downloaded != "Downloaded" }){
//All Tracks Downloaded
binding.btnDownloadAllSpotify.visibility = View.GONE
binding.downloadingFabSpotify.apply{
setImageResource(R.drawable.ic_tick)
visibility = View.VISIBLE
clearAnimation()
keepScreenOn = false
}
}
}
/** /**
* Basic Initialization * Basic Initialization
**/ **/
private fun initializeAll() { private fun initializeAll() {
webView = binding.webViewSpotify this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) adapter = TrackListAdapter(this.viewModel)
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java) sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer { this.viewModel.spotifyService = it
spotifyViewModel.spotifyService = it
}) })
SpotifyDownloadHelper.webView = binding.webViewSpotify DownloadHelper.youtubeMusicApi = youtubeMusicApi
SpotifyDownloadHelper.context = requireContext() DownloadHelper.sharedViewModel = sharedViewModel
SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel DownloadHelper.statusBar = binding.statusBar
SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify binding.trackList.adapter = adapter
binding.trackListSpotify.adapter = adapterSpotify (binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
(binding.trackListSpotify.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
/**
* Function to fetch all Images for using in mp3 tag.
**/
private suspend fun loadAllImages(trackList: List<Track>) {
trackList.forEach {
val imgUrl = it.album?.images?.get(0)?.url
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide
.with(requireContext())
.asFile()
.load(imgUri)
.listener(object: RequestListener<File> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File>?,
isFirstResource: Boolean
): Boolean {
Log.i("Glide","LoadFailed")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
sharedViewModel.uiScope.launch {
withContext(Dispatchers.IO){
try {
val file = File(
Environment.getExternalStorageDirectory(),
SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
)
resource?.copyTo(file)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return false
}
}).submit()
}
}
}
/**
* Configure Recycler View Adapter
**/
private fun adapterConfig(trackList: List<Track>){
adapterSpotify.ytDownloader = ytDownloader
adapterSpotify.spotifyViewModel = spotifyViewModel
adapterSpotify.submitList(trackList)
}
/**
* Util. Function to create toasts!
**/
private fun showToast(message:String){
Toast.makeText(context,message,Toast.LENGTH_SHORT).show()
}
/**
* Util. Function To Check Connection Status
**/
private fun isOnline(): Boolean {
val cm =
requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val netInfo = cm.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
} }
} }

View File

@ -17,85 +17,79 @@
package com.shabinder.spotiflyer.ui.spotify package com.shabinder.spotiflyer.ui.spotify
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.* import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.*
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
ViewModel(){
override var folderType:String = ""
override var subFolder:String = ""
var folderType:String = ""
var subFolder:String = ""
var trackList = MutableLiveData<MutableList<Track>>()
private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = loading }
var coverUrl = MutableLiveData<String>().apply { value = loading }
var spotifyService : SpotifyService? = null var spotifyService : SpotifyService? = null
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun spotifySearch(type:String,link: String){ fun spotifySearch(type:String,link: String){
when (type) { when (type) {
"track" -> { "track" -> {
uiScope.launch { uiScope.launch {
val trackObject = getTrackDetails(link) getTrackDetails(link)?.also {
folderType = "Tracks" folderType = "Tracks"
val tempTrackList = mutableListOf<Track>() if(File(finalOutputDir(it.name,folderType,subFolder)).exists()){//Download Already Present!!
if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!! it.downloaded = DownloadStatus.Downloaded
trackObject.downloaded = "Downloaded"
} }
tempTrackList.add(trackObject) trackList.value = listOf(it).toTrackDetailsList()
trackList.value = tempTrackList title.value = it.name
title.value = trackObject.name coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url
coverUrl.value = trackObject.album!!.images?.get(0)!!.url!!
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord( databaseDAO.insert(DownloadRecord(
type = "Track", type = "Track",
name = title.value!!, name = title.value!!,
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value!!,
totalFiles = tempTrackList.size, totalFiles = 1,
downloaded = trackObject.downloaded =="Downloaded", downloaded = it.downloaded == DownloadStatus.Downloaded,
directory = finalOutputDir(trackObject.name!!,folderType,subFolder) directory = finalOutputDir(it.name,folderType,subFolder)
)) ))
} }
} }
} }
}
"album" -> { "album" -> {
uiScope.launch { uiScope.launch {
val albumObject = getAlbumDetails(link) val albumObject = getAlbumDetails(link)
folderType = "Albums" folderType = "Albums"
subFolder = albumObject?.name.toString() subFolder = albumObject?.name.toString()
val tempTrackList = mutableListOf<Track>()
albumObject?.tracks?.items?.forEach { albumObject?.tracks?.items?.forEach {
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!! if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!!
it.downloaded = "Downloaded" it.downloaded = DownloadStatus.Downloaded
} }
it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url))) it.album = Album(images = listOf(Image(url = albumObject.images?.elementAtOrNull(1)?.url ?: albumObject.images?.elementAtOrNull(0)?.url )))
tempTrackList.add(it)
} }
trackList.value = tempTrackList trackList.value = albumObject?.tracks?.items?.toTrackDetailsList()
title.value = albumObject?.name title.value = albumObject?.name
coverUrl.value = albumObject?.images?.get(0)?.url coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url ?: albumObject?.images?.elementAtOrNull(0)?.url
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord( databaseDAO.insert(DownloadRecord(
type = "Album", type = "Album",
name = title.value!!, name = title.value!!,
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = tempTrackList.size, totalFiles = trackList.value?.size ?: 0,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size, downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
directory = finalOutputDir(type = folderType,subFolder = subFolder) directory = finalOutputDir(type = folderType,subFolder = subFolder)
)) ))
} }
@ -112,7 +106,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
playlistObject?.tracks?.items?.forEach { playlistObject?.tracks?.items?.forEach {
it.track?.let { it.track?.let {
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!! it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!!
it1.downloaded = "Downloaded" it1.downloaded = DownloadStatus.Downloaded
} }
tempTrackList.add(it1) tempTrackList.add(it1)
} }
@ -128,15 +122,15 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
moreTracksAvailable = !moreTracks?.next.isNullOrBlank() moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
} }
Log.i("Total Tracks Fetched",tempTrackList.size.toString()) Log.i("Total Tracks Fetched",tempTrackList.size.toString())
trackList.value = tempTrackList trackList.value = tempTrackList.toTrackDetailsList()
title.value = playlistObject?.name title.value = playlistObject?.name
coverUrl.value = playlistObject?.images?.get(0)!!.url!! coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url ?: playlistObject?.images?.firstOrNull()?.url.toString()
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord( databaseDAO.insert(DownloadRecord(
type = "Playlist", type = "Playlist",
name = title.value!!, name = title.value.toString(),
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value.toString(),
totalFiles = tempTrackList.size, totalFiles = tempTrackList.size,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size, downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
directory = finalOutputDir(type = folderType,subFolder = subFolder) directory = finalOutputDir(type = folderType,subFolder = subFolder)
@ -151,26 +145,39 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
} }
} }
@Suppress("DEPRECATION")
private fun List<Track>.toTrackDetailsList() = this.map {
TrackDetails(
title = it.name.toString(),
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
durationSec = (it.duration_ms/1000).toInt(),
albumArt = File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name,
year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href,
downloaded = it.downloaded,
source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()
)
}.toMutableList()
private suspend fun getTrackDetails(trackLink:String): Track?{ private suspend fun getTrackDetails(trackLink:String): Track?{
Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink") Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink")
return spotifyService?.getTrack(trackLink) return spotifyService?.getTrack(trackLink)?.value
} }
private suspend fun getAlbumDetails(albumLink:String): Album?{ private suspend fun getAlbumDetails(albumLink:String): Album?{
Log.i("Requesting","https://api.spotify.com/v1/albums/$albumLink") Log.i("Requesting","https://api.spotify.com/v1/albums/$albumLink")
return spotifyService?.getAlbum(albumLink) return spotifyService?.getAlbum(albumLink)?.value
} }
private suspend fun getPlaylistDetails(link:String): Playlist?{ private suspend fun getPlaylistDetails(link:String): Playlist?{
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link") Log.i("Requesting","https://api.spotify.com/v1/playlists/$link")
return spotifyService?.getPlaylist(link) return spotifyService?.getPlaylist(link)?.value
} }
private suspend fun getPlaylistTrackDetails(link:String,offset:Int = 0,limit:Int = 100): PagingObjectPlaylistTrack?{ private suspend fun getPlaylistTrackDetails(link:String,offset:Int = 0,limit:Int = 100): PagingObjectPlaylistTrack?{
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit") Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit")
return spotifyService?.getPlaylistTracks(link, offset, limit) return spotifyService?.getPlaylistTracks(link, offset, limit)?.value
} }
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
} }

View File

@ -21,46 +21,39 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.navArgs
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.bindImage import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint private const val sampleDomain2 = "youtu.be"
class YoutubeFragment : Fragment() { private const val sampleDomain1 = "youtube.com"
@AndroidEntryPoint
class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>() {
private lateinit var binding:YoutubeFragmentBinding
private lateinit var youtubeViewModel: YoutubeViewModel
private lateinit var sharedViewModel: SharedViewModel
private lateinit var adapter : YoutubeTrackListAdapter
private val sampleDomain1 = "youtube.com"
private val sampleDomain2 = "youtu.be"
@Inject lateinit var ytDownloader: YoutubeDownloader @Inject lateinit var ytDownloader: YoutubeDownloader
override lateinit var viewModel: YoutubeViewModel
override lateinit var adapter : TrackListAdapter
override var source: Source = Source.YouTube
override val args: YoutubeFragmentArgs by navArgs()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false) super.onCreateView(inflater, container, savedInstanceState)
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) adapter = TrackListAdapter(this.viewModel)
adapter = YoutubeTrackListAdapter() binding.trackList.adapter = adapter
YTDownloadHelper.context = requireContext()
YTDownloadHelper.statusBar = binding.StatusBarYoutube
binding.trackListYoutube.adapter = adapter
initializeLiveDataObservers()
val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link val link = args.link
@ -70,7 +63,11 @@ class YoutubeFragment : Fragment() {
private fun youtubeSearch(linkSearch:String) { private fun youtubeSearch(linkSearch:String) {
val link = linkSearch.removePrefix("https://").removePrefix("http://") val link = linkSearch.removePrefix("https://").removePrefix("http://")
if(!link.contains("playlist",true)){ if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
this.viewModel.getYTPlaylist(playlistId,ytDownloader)
}else{//Given Link is of a Video
var searchId = "error" var searchId = "error"
if(link.contains(sampleDomain1,true) ){ if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error") searchId = link.substringAfterLast("=","error")
@ -79,57 +76,48 @@ class YoutubeFragment : Fragment() {
searchId = link.substringAfterLast("/","error") searchId = link.substringAfterLast("/","error")
} }
if(searchId != "error") { if(searchId != "error") {
youtubeViewModel.getYTTrack(searchId,ytDownloader) this.viewModel.getYTTrack(searchId,ytDownloader)
binding.btnDownloadAllYoutube.setOnClickListener { }else{showMessage("Your Youtube Link is not of a Video!!")}
YTDownloadHelper.downloadFile(null,"YT_Downloads",
youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value)
}
}else{showToast("Your Youtube Link is not of a Video!!")}
}else(showToast("Your Youtube Link is not of a Video!!"))
} }
private fun initializeLiveDataObservers() { /*
/** * Download All Tracks
* CoverUrl Binding Observer!
* */ * */
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, Observer { binding.btnDownloadAll.setOnClickListener {
if(it!="Loading") bindImage(binding.youtubeCoverImage,it) if(!isOnline()){
}) showNoConnectionAlert()
return@setOnClickListener
/**
* 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
})
} }
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE
/** rotateAnim(binding.downloadingFab)
* Configure Recycler View Adapter
**/
private fun adapterConfig(list:List<Track>){
adapter.submitList(list)
}
/** for (track in this.viewModel.trackList.value?: listOf()){
* Util. Function to create toasts! if(track.downloaded != DownloadStatus.Downloaded){
**/ track.downloaded = DownloadStatus.Downloading
private fun showToast(message:String){ adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
Toast.makeText(context,message, Toast.LENGTH_SHORT).show() }
}
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()
)
}
}
} }
} }

View File

@ -17,79 +17,134 @@
package com.shabinder.spotiflyer.ui.youtube package com.shabinder.spotiflyer.ui.youtube
import android.annotation.SuppressLint
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.Artist import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.coroutines.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){
ViewModel(){ /*
* YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */
val ytTrack = MutableLiveData<Track>() override var folderType = "YT_Downloads"
val format = MutableLiveData<Format>() override var subFolder = ""
private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
var coverUrl = MutableLiveData<String>().apply { value = loading }
private var viewModelJob = Job() fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) if(!isOnline())return
try{
uiScope.launch(Dispatchers.IO) {
fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) { Log.i("YT Playlist",searchId)
uiScope.launch { val playlist = ytDownloader.getPlaylist(searchId)
withContext(Dispatchers.IO){ val playlistDetails = playlist.details()
Log.i("YT View Model",searchId) val name = playlistDetails.title()
val video = ytDownloader.getVideo(searchId) subFolder = removeIllegalChars(name).toString()
val detail = video?.details() val videos = playlist.videos()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg")
Log.i("YT View Model",detail.toString())
ytTrack.postValue(
Track(
id = searchId,
name = name,
artists = listOf<Artist>(Artist(name = detail?.author())),
duration_ms = detail?.lengthSeconds()?.times(1000)?.toLong()?:0,
ytCoverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
))
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg")
title.postValue( title.postValue(
if(name?.length!! > 17){"${name.subSequence(0,16)}..."}else{name} if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
) )
format.postValue(try { this@YoutubeViewModel.trackList.postValue(videos.map {
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format TrackDetails(
} catch (e: IndexOutOfBoundsException) { title = it.title(),
try { artists = listOf(it.author().toString()),
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format durationSec = it.lengthSeconds(),
} catch (e: IndexOutOfBoundsException) { albumArt = File(
try { Environment.getExternalStorageDirectory(),
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format defaultDir + ".Images/" + it.videoId() + ".jpeg"
} catch (e: IndexOutOfBoundsException) { ),
Log.i("YTDownloader", e.toString()) source = Source.YouTube,
null albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = it.title(),
type = folderType,
subFolder = subFolder
)).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
}
)
}.toMutableList())
withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord(
type = "PlayList",
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
link = "https://www.youtube.com/playlist?list=$searchId",
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg",
totalFiles = videos.size,
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
))
} }
} }
}) }catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){
showMessage("An Error Occurred While Processing!")
}
}
@SuppressLint("DefaultLocale")
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) {
if(!isOnline())return
try{
uiScope.launch(Dispatchers.IO) {
Log.i("YT Video",searchId)
val video = ytDownloader.getVideo(searchId)
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
val detail = video?.details()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
Log.i("YT View Model",detail.toString())
this@YoutubeViewModel.trackList.postValue(
listOf(
TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0,
albumArt = File(
Environment.getExternalStorageDirectory(),
"$defaultDir.Images/$searchId.jpeg"
),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
)
).toMutableList()
)
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
databaseDAO.insert(DownloadRecord( databaseDAO.insert(DownloadRecord(
type = "Track", type = "Track",
name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name},
link = "https://www.youtube.com/watch?v=$searchId", link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg", coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1, totalFiles = 1,
downloaded = false, downloaded = false,
directory = finalOutputDir(type = "YT_Downloads") directory = finalOutputDir(type = "YT_Downloads")
)) ))
} }
} }
} catch (e:com.github.kiulian.downloader.YoutubeException){
showMessage("An Error Occurred While Processing!")
} }
} }
} }

View File

@ -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")}
}

View File

@ -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) }
}

View File

@ -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()
}
}

View File

@ -18,11 +18,15 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.content.Context import android.content.Context
import android.os.Environment
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.App import com.shabinder.spotiflyer.App
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shreyaspatil.easyupipayment.EasyUpiPayment import com.shreyaspatil.easyupipayment.EasyUpiPayment
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
@ -36,23 +40,35 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import javax.inject.Singleton import javax.inject.Singleton
@InstallIn(ApplicationComponent::class) @InstallIn(ApplicationComponent::class)
@Module @Module
object Provider { object Provider {
val mainActivity: MainActivity = MainActivity.getInstance()
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
@Provides @Provides
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{ fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
return DownloadRecordDatabase.getInstance(appContext).databaseDAO return DownloadRecordDatabase.getInstance(appContext).databaseDAO
} }
@Provides
@Singleton
fun getYTDownloader():YoutubeDownloader{
return YoutubeDownloader()
}
@Provides @Provides
@Singleton @Singleton
fun provideUpi():EasyUpiPayment { fun provideUpi():EasyUpiPayment {
return EasyUpiPayment.Builder(MainActivity.getInstance()) return EasyUpiPayment.Builder(mainActivity)
.setPayeeVpa("technoshab@paytm") .setPayeeVpa("technoshab@paytm")
.setPayeeName("Shabinder Singh") .setPayeeName("Shabinder Singh")
.setTransactionId("UNIQUE_TRANSACTION_ID") .setTransactionId("UNIQUE_TRANSACTION_ID")
@ -72,29 +88,59 @@ object Provider {
@Provides @Provides
@Singleton @Singleton
fun getYTDownloader():YoutubeDownloader{ fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
return YoutubeDownloader()
}
@Provides
@Singleton
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder() val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient2.addInterceptor(Interceptor { chain -> .addInterceptor(Interceptor { chain ->
val request: Request = val request: Request =
chain.request().newBuilder().addHeader( chain.request().newBuilder()
.addHeader(
"Authorization", "Authorization",
"Basic ${android.util.Base64.encodeToString("${App.clientId}:${App.clientSecret}".toByteArray(),android.util.Base64.NO_WRAP)}" "Basic ${
android.util.Base64.encodeToString(
"${App.clientId}:${App.clientSecret}".toByteArray(),
android.util.Base64.NO_WRAP
)
}"
).build() ).build()
chain.proceed(request) chain.proceed(request)
}) }).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.baseUrl("https://accounts.spotify.com/") .baseUrl("https://accounts.spotify.com/")
.client(httpClient2.build()) .client(httpClient2.build())
.addConverterFactory(MoshiConverterFactory.create(getMoshi())) .addConverterFactory(MoshiConverterFactory.create(moshi))
.build() .build()
return retrofit.create(SpotifyServiceTokenRequest::class.java) return retrofit.create(SpotifyServiceTokenRequest::class.java)
} }
@Provides
@Singleton
fun okHttpClient():OkHttpClient{
return OkHttpClient.Builder()
.addInterceptor(NetworkInterceptor())
.build()
}
@Provides
@Singleton
fun getGaanaInterface(moshi: Moshi,okHttpClient: OkHttpClient):GaanaInterface{
val retrofit = Retrofit.Builder()
.baseUrl("https://api.gaana.com/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(GaanaInterface::class.java)
}
@Provides
@Singleton
fun getYoutubeMusicApi(moshi: Moshi): YoutubeMusicApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://music.youtube.com/youtubei/v1/")
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(YoutubeMusicApi::class.java)
}
} }

View File

@ -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()
}
}
}
}

View File

@ -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
View File

@ -17,16 +17,288 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.worker.ForegroundService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) {
val serviceIntent = Intent(context, ForegroundService::class.java)
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
fun startService(context:Context?,objects:ArrayList<DownloadObject>? = null ) {
val serviceIntent = Intent(context, ForegroundService::class.java)
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{
return Environment.getExternalStorageDirectory().toString() + File.separator +
defaultDir + removeIllegalChars(type) + File.separator +
(if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator}
+ itemName?.let { removeIllegalChars(it) + extension})
}
/**
* Util. Function To Check Connection Status
**/
@Suppress("DEPRECATION")
fun isOnline(): Boolean {
var result = false
val connectivityManager =
mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
} else {
val netInfo =
(mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
result = netInfo != null && netInfo.isConnected
}
}
return result
}
fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false , isError:Boolean = false){
CoroutineScope(Dispatchers.Main).launch{
Snackbar.make(
mainActivity.snackBarAnchor,
message,
if (long) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
).apply {
setAction("Ok") {
dismiss()
}
setActionTextColor(ContextCompat.getColor(mainActivity,R.color.black))
when{
isSuccess -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.successGreen))
isError -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.errorRed))
}
}.show()
}
}
fun rotateAnim(view: View){
val rotate = RotateAnimation(
0F, 360F,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
)
rotate.duration = 1000
rotate.repeatCount = Animation.INFINITE
rotate.repeatMode = Animation.INFINITE
rotate.interpolator = LinearInterpolator()
view.animation = rotate
}
fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch {
mainActivity.apply {
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text))
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
// Respond to neutral button press
}.show()
}
}
}
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide
.with(imgView)
.asFile()
.load(imgUri)
.placeholder(R.drawable.ic_song_placeholder)
.error(R.drawable.ic_musicplaceholder)
.listener(object:RequestListener<File>{
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File>?,
isFirstResource: Boolean
): Boolean {
Log.i("Glide","LoadFailed")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
CoroutineScope(Dispatchers.Main).launch {
try {
val file = when(source){
Source.Spotify->{
File(
Environment.getExternalStorageDirectory(),
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
)
}
Source.YouTube->{
//Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
// We Are Naming using "$searchId"
File(
Environment.getExternalStorageDirectory(),
defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
)
}
Source.Gaana -> {
File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (imgUrl.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
}
else -> File(
Environment.getExternalStorageDirectory(),
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
)
}
// the File to save , append increasing numeric counter to prevent files from getting overwritten.
resource?.copyTo(file)
Glide.with(imgView)
.load(file)
.placeholder(R.drawable.ic_song_placeholder)
.into(imgView)
} catch (e: IOException) {
e.printStackTrace()
}
}
return false
}
}).submit()
}
}
/**
*Extension Function For Copying Files!
**/
fun File.copyTo(file: File) {
inputStream().use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
fun createDirectory(dir:String){
val yourAppDir = File(Environment.getExternalStorageDirectory(),
dir)
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
{ // create empty directory
if (yourAppDir.mkdirs())
{Log.i("CreateDir","App dir created")}
else
{Log.w("CreateDir","Unable to create app dir!")}
}
else
{Log.i("CreateDir","App dir already exists")}
}
/**
* Removing Illegal Chars from File Name
* **/
fun removeIllegalChars(fileName: String): String? {
val illegalCharArray = charArrayOf(
'/',
'\n',
'\r',
'\t',
'\u0000',
'\u000C',
'`',
'?',
'*',
'\\',
'<',
'>',
'|',
'\"',
'.',
'-',
'\''
)
var name = fileName
for (c in illegalCharArray) {
name = fileName.replace(c, '_')
}
name = name.replace("\\s".toRegex(), "_")
name = name.replace("\\)".toRegex(), "")
name = name.replace("\\(".toRegex(), "")
name = name.replace("\\[".toRegex(), "")
name = name.replace("]".toRegex(), "")
name = name.replace("\\.".toRegex(), "")
name = name.replace("\"".toRegex(), "")
name = name.replace("\'".toRegex(), "")
name = name.replace(":".toRegex(), "")
name = name.replace("\\|".toRegex(), "")
return name
}
fun createDirectories() { fun createDirectories() {
createDirectory(SpotifyDownloadHelper.defaultDir) createDirectory(defaultDir)
createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/") createDirectory(defaultDir + ".Images/")
createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/") createDirectory(defaultDir + "Tracks/")
createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/") createDirectory(defaultDir + "Albums/")
createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/") createDirectory(defaultDir + "Playlists/")
createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/") createDirectory(defaultDir + "YT_Downloads/")
} }
fun getEmojiByUnicode(unicode: Int): String? { fun getEmojiByUnicode(unicode: Int): String? {
return String(Character.toChars(unicode)) return String(Character.toChars(unicode))
} }
/*
internal val nullOnEmptyConverterFactory = object : Converter.Factory() {
fun converterFactory() = this
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
) = object : Converter<ResponseBody, Any?> {
val nextResponseBodyConverter =
retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
override fun convert(value: ResponseBody) =
if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
}
}*/

View File

@ -17,6 +17,7 @@
package com.shabinder.spotiflyer.worker package com.shabinder.spotiflyer.worker
import android.annotation.SuppressLint
import android.app.* import android.app.*
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -28,27 +29,37 @@ import android.os.*
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import com.arthenica.mobileffmpeg.Config import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
import com.arthenica.mobileffmpeg.FFmpeg import com.arthenica.mobileffmpeg.FFmpeg
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.mpatric.mp3agic.ID3v1Tag import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.copyTo
import com.tonyodev.fetch2.* import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException
import java.util.*
@Suppress("DEPRECATION")
class ForegroundService : Service(){ class ForegroundService : Service(){
private val tag = "Foreground Service" private val tag = "Foreground Service"
private val channelId = "ForegroundDownloaderService" private val channelId = "ForegroundDownloaderService"
@ -56,22 +67,21 @@ class ForegroundService : Service(){
private var total = 0 //Total Downloads Requested private var total = 0 //Total Downloads Requested
private var converted = 0//Total Files Converted private var converted = 0//Total Files Converted
private var downloaded = 0//Total Files downloaded private var downloaded = 0//Total Files downloaded
private var fetch:Fetch? = null private lateinit var fetch:Fetch
private var downloadManager : DownloadManager? = null private lateinit var ytDownloader: YoutubeDownloader
private var downloadList = mutableListOf<DownloadObject>() private lateinit var downloadManager : DownloadManager
private var serviceJob = Job() private var serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val requestMap = mutableMapOf<Request,Track>() private val requestMap = mutableMapOf<Request, TrackDetails>()
private val downloadMap = mutableMapOf<String,Track>()
private var speed :Long = 0 private var speed :Long = 0
private var defaultDirectory = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator private var defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
private val parentDirectory = File(Environment.getExternalStorageDirectory(), private val parentDirectory = File(Environment.getExternalStorageDirectory(),
defaultDirectory+File.separator defaultDir +File.separator
) )
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
var notificationLine = 0 var notificationLine = 0
val messageList = mutableListOf<String>("","","","") val messageList = mutableListOf("","","","")
private var pendingIntent:PendingIntent? = null private var pendingIntent:PendingIntent? = null
@ -88,7 +98,7 @@ class ForegroundService : Service(){
0, notificationIntent, 0 0, notificationIntent, 0
) )
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
ytDownloader = YoutubeDownloader()
val fetchConfiguration = val fetchConfiguration =
FetchConfiguration.Builder(this) FetchConfiguration.Builder(this)
.setDownloadConcurrentLimit(4) .setDownloadConcurrentLimit(4)
@ -97,90 +107,41 @@ class ForegroundService : Service(){
Fetch.setDefaultInstanceConfiguration(fetchConfiguration) Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
fetch = Fetch.getDefaultInstance() fetch = Fetch.getDefaultInstance()
// fetch?.enableLogging(true) fetch.addListener(fetchListener)
fetch?.addListener(fetchListener)
//clearing all not completed Downloads //clearing all not completed Downloads
//Starting fresh //Starting fresh
fetch?.removeAll() fetch.removeAll()
startForeground() startForeground()
} }
/** @SuppressLint("WakelockTimeout")
*Starting Service with Notification as Foreground!
**/
private fun startForeground() {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service")
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.down_arrowbw)
.setNotificationSilent()
.setSubText("Total: $total Completed:$converted")
.setStyle(NotificationCompat.InboxStyle()
.setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0])
.addLine(messageList[1])
.addLine(messageList[2])
.addLine(messageList[3]))
.setContentIntent(pendingIntent)
.build()
startForeground(notificationId, notification)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT)
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started // Send a notification that service is started
Log.i(tag,"Service Started.") Log.i(tag,"Service Started.")
startForeground() startForeground()
//do heavy work on a background thread val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
//val list = intent.getSerializableExtra("list") as List<Any?> val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList"))
// val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
// Log.i(tag,"Intent List Size: ${list!!.size}")
val obj = intent.getParcelableExtra<DownloadObject>("object") ?: intent.extras?.getParcelable<DownloadObject>("object")
obj?.let {
total ++
// Log.i(tag,"Intent List Size: ${list!!.size}")
updateNotification()
serviceScope.launch {
val request= Request(obj.url, obj.outputDir)
request.priority = Priority.NORMAL
request.networkType = NetworkType.ALL
fetch!!.enqueue(request, imagesList?.let{
{ serviceScope.launch {
obj.track?.let { it1 -> requestMap.put(it, it1) } loadAllImages(it)
downloadList.remove(obj)
Log.i(tag, "Enqueuing Download")
},
{
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
)
} }
} }
downloadObjects?.let {
total += downloadObjects.size
updateNotification()
downloadAllTracks(downloadObjects)
}
//Wake locks and misc tasks from here : //Wake locks and misc tasks from here :
return if (isServiceStarted){ return if (isServiceStarted){
//Service Already Started
START_STICKY START_STICKY
} else{ } else{
Log.i(tag,"Starting the foreground service task") Log.i(tag,"Starting the foreground service task")
isServiceStarted = true isServiceStarted = true
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
@ -191,9 +152,53 @@ class ForegroundService : Service(){
} }
} }
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){
serviceScope.launch(Dispatchers.IO) {
for(downloadObj in downloadObjects){
try {
val video = ytDownloader.getVideo(downloadObj.ytVideoId)
val format: Format? = try {
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
Log.i("YTDownloader", e.toString())
null
}
}
}
format?.let {
val url: String = format.url()
Log.i("DHelper Link Found", url)
serviceScope.launch {
val request= Request(url, downloadObj.outputFile)
request.priority = Priority.NORMAL
request.networkType = NetworkType.ALL
fetch.enqueue(request,
{
requestMap[it] = downloadObj.trackDetails
Log.i(tag, "Enqueuing Download")
},
{
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
)
}
}
}catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString())
}
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if(downloadMap.isEmpty() && converted == total){ if(converted == total){
Handler().postDelayed({ Handler().postDelayed({
Log.i(tag,"Service destroyed.") Log.i(tag,"Service destroyed.")
deleteFile(parentDirectory) deleteFile(parentDirectory)
@ -203,25 +208,11 @@ class ForegroundService : Service(){
} }
} }
private fun releaseWakeLock() {
Log.i(tag,"Releasing Wake Lock")
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
} catch (e: Exception) {
Log.i(tag,"Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
override fun onTaskRemoved(rootIntent: Intent?) { override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
if(downloadMap.isEmpty() && converted == total ){ if(converted == total ){
Log.i(tag,"Service Removed.") Log.i(tag,"Service Removed.")
deleteFile(parentDirectory)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true) stopForeground(true)
} else { } else {
@ -230,25 +221,6 @@ class ForegroundService : Service(){
} }
} }
/**
* Deleting All Residual Files except Mp3 Files
**/
private fun deleteFile(dir:File) {
Log.i(tag,"Starting Deletions in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
if (file.isDirectory) {
deleteFile(file)
} else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){
// Log.i(tag,"deleting ${file.path}")
file.delete()
}
}
}
}
}
/** /**
* Fetch Listener/ Responsible for Fetch Behaviour * Fetch Listener/ Responsible for Fetch Behaviour
@ -277,23 +249,23 @@ class ForegroundService : Service(){
val track = requestMap[download.request] val track = requestMap[download.request]
when(notificationLine){ when(notificationLine){
0 -> { 0 -> {
messageList[0] = "Downloading ${track?.name}" messageList[0] = "Downloading ${track?.title}"
notificationLine = 1 notificationLine = 1
} }
1 -> { 1 -> {
messageList[1] = "Downloading ${track?.name}" messageList[1] = "Downloading ${track?.title}"
notificationLine = 2 notificationLine = 2
} }
2-> { 2-> {
messageList[2] = "Downloading ${track?.name}" messageList[2] = "Downloading ${track?.title}"
notificationLine = 3 notificationLine = 3
} }
3 -> { 3 -> {
messageList[3] = "Downloading ${track?.name}" messageList[3] = "Downloading ${track?.title}"
notificationLine = 0 notificationLine = 0
} }
} }
Log.i(tag,"${track?.name} Download Started") Log.i(tag,"${track?.title} Download Started")
updateNotification() updateNotification()
} }
@ -312,32 +284,27 @@ class ForegroundService : Service(){
override fun onCompleted(download: Download) { override fun onCompleted(download: Download) {
val track = requestMap[download.request] val track = requestMap[download.request]
for (message in messageList){ for (message in messageList){
if( message == "Downloading ${track?.name}"){ if( message == "Downloading ${track?.title}"){
//Remove Downloading Status from Notification
messageList[messageList.indexOf(message)] = "" messageList[messageList.indexOf(message)] = ""
} }
} }
//Notify Download Completed
val intent = Intent()
.setAction("track_download_completed")
.putExtra("track",track)
this@ForegroundService.sendBroadcast(intent)
serviceScope.launch { serviceScope.launch {
try{ try{
convertToMp3(download.file, track!!) track?.let { convertToMp3(download.file, it) }
Log.i(tag,"${track.name} Download Completed") Log.i(tag,"${track?.title} Download Completed")
}catch (e:KotlinNullPointerException }catch (e:KotlinNullPointerException
){ ){
Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!") Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!")
Log.i(tag,"${track?.name} Requesting Download thru Android DM") Log.i(tag,"${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url,download.request.file, track!!)
downloaded++ downloaded++
requestMap.remove(download.request) requestMap.remove(download.request)
} }
} }
speed = 0 speed = 0
updateNotification() // updateNotification()
} }
override fun onDeleted(download: Download) { override fun onDeleted(download: Download) {
@ -357,7 +324,7 @@ class ForegroundService : Service(){
val track = requestMap[download.request] val track = requestMap[download.request]
downloaded++ downloaded++
Log.i(tag,download.error.throwable.toString()) Log.i(tag,download.error.throwable.toString())
Log.i(tag,"${track?.name} Requesting Download thru Android DM") Log.i(tag,"${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url,download.request.file, track!!)
requestMap.remove(download.request) requestMap.remove(download.request)
} }
@ -374,9 +341,9 @@ class ForegroundService : Service(){
downloadedBytesPerSecond: Long downloadedBytesPerSecond: Long
) { ) {
val track = requestMap[download.request] val track = requestMap[download.request]
Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec") Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec")
speed = (downloadedBytesPerSecond/1000) speed = (downloadedBytesPerSecond/1000)
updateNotification() // updateNotification()
} }
} }
@ -384,7 +351,7 @@ class ForegroundService : Service(){
/** /**
* If fetch Fails , Android Download Manager To RESCUE!! * If fetch Fails , Android Download Manager To RESCUE!!
**/ **/
fun downloadUsingDM(url:String, outputDir:String, track: Track){ fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){
val uri = Uri.parse(url) val uri = Uri.parse(url)
val request = DownloadManager.Request(uri) val request = DownloadManager.Request(uri)
.setAllowedNetworkTypes( .setAllowedNetworkTypes(
@ -392,14 +359,14 @@ class ForegroundService : Service(){
DownloadManager.Request.NETWORK_MOBILE DownloadManager.Request.NETWORK_MOBILE
) )
.setAllowedOverRoaming(false) .setAllowedOverRoaming(false)
.setTitle(track.name) .setTitle(track.title)
.setDescription("Spotify Downloader Working Up here...") .setDescription("Spotify Downloader Working Up here...")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix( .setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix(
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator
)) ))
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
//Start Download //Start Download
val downloadID = downloadManager?.enqueue(request) val downloadID = downloadManager.enqueue(request)
Log.i("DownloadManager", "Download Request Sent") Log.i("DownloadManager", "Download Request Sent")
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() { val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
@ -421,7 +388,7 @@ class ForegroundService : Service(){
/** /**
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata) *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/ **/
fun convertToMp3(filePath: String, track: Track){ fun convertToMp3(filePath: String, track: TrackDetails){
val m4aFile = File(filePath) val m4aFile = File(filePath)
FFmpeg.executeAsync( FFmpeg.executeAsync(
@ -444,7 +411,7 @@ class ForegroundService : Service(){
} }
} }
private fun writeMp3Tags(filePath:String, track: Track){ private fun writeMp3Tags(filePath:String, track: TrackDetails){
var mp3File = Mp3File(filePath) var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File) mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File,track) mp3File = setId3v1Tags(mp3File,track)
@ -457,11 +424,17 @@ class ForegroundService : Service(){
newFile.renameTo(file) newFile.renameTo(file)
converted++ converted++
updateNotification() updateNotification()
//Notify Download Completed
val intent = Intent()
.setAction("track_download_completed")
.putExtra("track",track)
this@ForegroundService.sendBroadcast(intent)
//All tasks completed (REST IN PEACE) //All tasks completed (REST IN PEACE)
if(converted == total){ if(converted == total){
onDestroy() onDestroy()
} }
} }
/** /**
@ -475,7 +448,7 @@ class ForegroundService : Service(){
.setSubText("Total: $total Completed:$converted") .setSubText("Total: $total Completed:$converted")
.setNotificationSilent() .setNotificationSilent()
.setStyle(NotificationCompat.InboxStyle() .setStyle(NotificationCompat.InboxStyle()
.setBigContentTitle("Speed: $speed KB/s") // .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0]) .addLine(messageList[0])
.addLine(messageList[1]) .addLine(messageList[1])
.addLine(messageList[2]) .addLine(messageList[2])
@ -486,64 +459,42 @@ class ForegroundService : Service(){
} }
/** /**
*Modifying Mp3 Tags with MetaData! *Modifying Mp3 com.shabinder.spotiflyer.models.gaana.Tags with MetaData!
**/ **/
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File { private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
val id3v1Tag = ID3v1Tag() val id3v1Tag = ID3v1Tag().apply {
id3v1Tag.track = track.disc_number.toString() artist = track.artists.joinToString(",")
val artistsList = mutableListOf<String>() title = track.title
track.artists?.forEach { artistsList.add(it!!.name!!) } album = track.albumName
id3v1Tag.artist = artistsList.joinToString() year = track.year
id3v1Tag.title = track.name comment = "Genres:${track.comment}"
id3v1Tag.album = track.album?.name }
id3v1Tag.year = track.album?.release_date
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
mp3File.id3v1Tag = id3v1Tag mp3File.id3v1Tag = id3v1Tag
return mp3File return mp3File
} }
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File { private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File {
val id3v2Tag = ID3v24Tag() val id3v2Tag = ID3v24Tag().apply {
id3v2Tag.track = track.disc_number.toString() artist = track.artists.joinToString(",")
val artistsList = mutableListOf<String>() title = track.title
track.artists?.forEach { artistsList.add(it!!.name!!) } album = track.albumName
id3v2Tag.artist = artistsList.joinToString() year = track.year
id3v2Tag.title = track.name comment = "Genres:${track.comment}"
id3v2Tag.album = track.album?.name lyrics = "Gonna Implement Soon"
id3v2Tag.year = track.album?.release_date url = track.trackUrl
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}" }
id3v2Tag.lyrics = "Gonna Implement Soon" val bytesArray = ByteArray(track.albumArt.length().toInt())
val copyrights = mutableListOf<String>() try{
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) } val fis = FileInputStream(track.albumArt)
id3v2Tag.copyright = copyrights.joinToString()
id3v2Tag.url = track.href
track.ytCoverUrl?.let {
val file = File(
Environment.getExternalStorageDirectory(),
SpotifyDownloadHelper.defaultDir +".Images/" + it.substringAfterLast('/',it) + ".jpeg")
Log.i("Mp3Tags editing Tags",file.path)
//init array with file length
val bytesArray = ByteArray(file.length().toInt())
val fis = FileInputStream(file)
fis.read(bytesArray) //read file into bytes[] fis.read(bytesArray) //read file into bytes[]
fis.close() fis.close()
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
}catch (e:java.io.FileNotFoundException){
Log.i("Error","Couldn't Write Mp3 Album Art")
} }
track.album?.let {
val file = File(
Environment.getExternalStorageDirectory(),
SpotifyDownloadHelper.defaultDir +".Images/" + (it.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
Log.i("Mp3Tags editing Tags",file.path)
//init array with file length
val bytesArray = ByteArray(file.length().toInt())
val fis = FileInputStream(file)
fis.read(bytesArray) //read file into bytes[]
fis.close()
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
}
id3v2Tag.albumImage
mp3file.id3v2Tag = id3v2Tag mp3file.id3v2Tag = id3v2Tag
return mp3file return mp3file
} }
private fun removeAllTags(mp3file: Mp3File): Mp3File { private fun removeAllTags(mp3file: Mp3File): Mp3File {
if (mp3file.hasId3v1Tag()) { if (mp3file.hasId3v1Tag()) {
mp3file.removeId3v1Tag() mp3file.removeId3v1Tag()
@ -557,4 +508,146 @@ class ForegroundService : Service(){
return mp3file return mp3file
} }
private fun releaseWakeLock() {
Log.i(tag,"Releasing Wake Lock")
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
} catch (e: Exception) {
Log.i(tag,"Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
/**
*Starting Service with Notification as Foreground!
**/
private fun startForeground() {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service")
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.down_arrowbw)
.setNotificationSilent()
.setSubText("Total: $total Completed:$converted")
.setStyle(NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0])
.addLine(messageList[1])
.addLine(messageList[2])
.addLine(messageList[3]))
.setContentIntent(pendingIntent)
.build()
startForeground(notificationId, notification)
}
@Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT)
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
/**
* Deleting All Residual Files except Mp3 Files
**/
private fun deleteFile(dir:File) {
Log.i(tag,"Starting Deletions in ${dir.path} ")
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
if (file.isDirectory) {
deleteFile(file)
} else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){
Log.i(tag,"deleting ${file.path}")
file.delete()
}
}
}
}
}
/**
* Function to fetch all Images for use in mp3 tags.
**/
private suspend fun loadAllImages(urlList: ArrayList<String>) {
/*
* Last Element of this List defines Its Source
* */
val source = urlList.last()
for (url in urlList.subList(0,urlList.size-2)) {
val imgUri = url.toUri().buildUpon().scheme("https").build()
Glide
.with(this)
.asFile()
.load(imgUri)
.listener(object: RequestListener<File> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File>?,
isFirstResource: Boolean
): Boolean {
Log.i("Glide","LoadFailed")
return false
}
override fun onResourceReady(
resource: File?,
model: Any?,
target: Target<File>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
serviceScope.launch {
withContext(Dispatchers.IO){
try {
val file = when(source){
"spotify" ->{
File(
Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg"
)
}
"youtube" ->{
File(
Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg"
)
}
"gaana" -> {
File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
}
else -> File(
Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg")
}
resource?.copyTo(file)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return false
}
}).submit()
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -16,7 +16,8 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<shape xmlns:android="http://schemas.android.com/apk/res/android" > <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/black"/>
<gradient <gradient
android:angle="90" android:angle="90"
android:centerColor="#0F6200FF" android:centerColor="#0F6200FF"

View 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>

Binary file not shown.

Binary file not shown.

View File

@ -15,10 +15,9 @@
~ You should have received a copy of the GNU General Public License ~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -27,14 +26,10 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="@drawable/text_background_accented" style="@style/Widget.AppCompat.TextView.Gradient"
android:drawablePadding="5dp" android:drawablePadding="5dp"
android:fontFamily="@font/raleway_semibold" android:fontFamily="@font/raleway_semibold"
android:gravity="center"
android:padding="8dp"
android:text=" Download History " android:text=" Download History "
android:textAlignment="center"
android:textColor="#E1FFFFFF"
android:textSize="21sp" android:textSize="21sp"
app:drawableStartCompat="@drawable/ic_history" app:drawableStartCompat="@drawable/ic_history"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -43,6 +38,7 @@
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
android:background="@color/black"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@ -60,6 +56,11 @@
android:icon="@drawable/ic_spotify_logo" android:icon="@drawable/ic_spotify_logo"
android:text="Spotify" /> android:text="Spotify" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/gaana"
android:text="Gaana" />
<com.google.android.material.tabs.TabItem <com.google.android.material.tabs.TabItem
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -82,4 +83,3 @@
app:layout_constraintTop_toBottomOf="@+id/tabLayout" /> app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -17,15 +17,8 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<variable
name="downloadRecord"
type="com.shabinder.spotiflyer.database.DownloadRecord" />
</data>
<RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="92dp" android:layout_height="92dp"
android:background="#000000" android:background="#000000"
@ -101,5 +94,4 @@
android:tint="@null" /> android:tint="@null" />
</RelativeLayout> </RelativeLayout>
</layout>

View File

@ -16,38 +16,31 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainActivity" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <View
android:id="@+id/message" android:id="@+id/snackBarPosition"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:layout_marginBottom="16dp"
android:background="@drawable/text_background_accented" android:background="@drawable/transparent"
android:padding="5dp"
android:visibility="gone"
android:paddingTop="6dp"
android:text="Authentication Needed"
android:textColor="@color/colorPrimary"
android:textSize="10dp"
android:textStyle="bold" android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<fragment <fragment
android:id="@+id/NavHostFragment" android:id="@+id/navHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:defaultNavHost="true" app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/message" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -55,4 +48,3 @@
tools:ignore="FragmentTagUsage" /> tools:ignore="FragmentTagUsage" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -15,46 +15,40 @@
~ You should have received a copy of the GNU General Public License ~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ScrollView <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fillViewport="true" android:fillViewport="true"
tools:ignore="HardcodedText"
> >
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<EditText <EditText
android:id="@+id/linkSearch" android:id="@+id/linkSearch"
android:layout_width="wrap_content" style="@style/Widget.AppCompat.TextView.Gradient"
android:layout_height="46dp" android:layout_height="46dp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:background="@drawable/text_background_accented"
android:ems="10" android:ems="10"
android:hint="Paste Link here" android:hint="Paste Link here"
android:inputType="text"
android:padding="8dp"
android:textAlignment="center"
android:textColor="@color/white"
android:textColorHint="@color/grey"
android:textSize="19sp"
app:layout_constraintEnd_toStartOf="@+id/btn_search" app:layout_constraintEnd_toStartOf="@+id/btn_search"
app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:importantForAutofill="no"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_search" android:id="@+id/btn_search"
android:layout_width="wrap_content" style="@style/Widget.AppCompat.Button.Colored.Gradient"
android:layout_height="44dp" android:layout_height="44dp"
android:background="@drawable/btn_design" android:fontFamily="@font/nunito_sans_light"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:text=" Search " android:text=" Search "
android:textColor="@color/black" android:textStyle="bold"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@+id/linkSearch" app:layout_constraintBottom_toBottomOf="@+id/linkSearch"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/linkSearch" app:layout_constraintStart_toEndOf="@+id/linkSearch"
@ -74,18 +68,15 @@
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linkSearch" /> app:layout_constraintTop_toBottomOf="@+id/linkSearch"
tools:ignore="UnusedAttribute" />
<ImageButton <ImageButton
android:id="@+id/btn_history" android:id="@+id/btn_history"
android:layout_width="40dp" style="@style/Widget.AppCompat.ImageButton.40dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/transparent"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/appLogo" app:layout_constraintBottom_toBottomOf="@+id/appLogo"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:contentDescription="Open Download History Button"
app:srcCompat="@drawable/ic_history"/> app:srcCompat="@drawable/ic_history"/>
<TextView <TextView
@ -94,16 +85,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:fontFamily="@font/raleway_semibold" android:fontFamily="@font/raleway_semibold"
android:gravity="end"
android:text='"SpotiFlyer"' android:text='"SpotiFlyer"'
android:textAlignment="viewEnd" android:textColor="@color/colorAccent"
android:textColor="#9AB3FF"
android:textSize="40sp" android:textSize="40sp"
android:typeface="normal"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appLogo" /> app:layout_constraintTop_toBottomOf="@id/appLogo"
/>
<TextView <TextView
android:id="@+id/appSubTitle" android:id="@+id/appSubTitle"
@ -119,49 +107,36 @@
app:layout_constraintStart_toStartOf="@+id/appName" app:layout_constraintStart_toStartOf="@+id/appName"
app:layout_constraintTop_toBottomOf="@+id/appName" /> app:layout_constraintTop_toBottomOf="@+id/appName" />
<ImageButton
<TextView android:id="@+id/btn_spotify"
android:id="@+id/platforms" style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:fontFamily="@font/raleway_semibold" android:contentDescription="Open Spotify App Button"
android:text="Supports: " android:src="@drawable/ic_spotify_logo"
android:textAlignment="center" app:layout_constraintEnd_toStartOf="@+id/btn_Gaana"
android:textColor="#9AB3FF" android:padding="6dp"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@+id/btn_spotify"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="@+id/appSubTitle"
app:layout_constraintTop_toBottomOf="@+id/appSubTitle"/> app:layout_constraintTop_toBottomOf="@+id/appSubTitle"/>
<ImageButton
android:id="@+id/btn_spotify"
android:layout_width="46dp"
android:layout_height="46dp"
android:layout_marginEnd="2dp"
android:background="@color/black"
android:padding="5dp"
android:scaleType="fitCenter"
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" />
<ImageButton <ImageButton
android:id="@+id/btn_youtube" android:id="@+id/btn_youtube"
android:layout_width="52dp" style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:layout_height="52dp" android:contentDescription="Open Youtube App Button"
android:background="@color/black"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_youtube" android:src="@drawable/ic_youtube"
app:layout_constraintBottom_toBottomOf="@+id/btn_spotify" app:layout_constraintBottom_toBottomOf="@+id/btn_spotify"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="@+id/appSubTitle"
app:layout_constraintStart_toEndOf="@+id/btn_Gaana"
app:layout_constraintTop_toTopOf="@+id/btn_spotify"/>
<ImageButton
android:id="@+id/btn_Gaana"
style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:contentDescription="Open Gaana App Button"
android:padding="6dp"
android:src="@drawable/gaana"
app:layout_constraintBottom_toBottomOf="@+id/btn_spotify"
app:layout_constraintEnd_toStartOf="@+id/btn_youtube"
app:layout_constraintStart_toEndOf="@+id/btn_spotify" app:layout_constraintStart_toEndOf="@+id/btn_spotify"
app:layout_constraintTop_toTopOf="@+id/btn_spotify" /> app:layout_constraintTop_toTopOf="@+id/btn_spotify" />
@ -170,15 +145,15 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:gravity="center"
android:text="Usage Instructions!" android:text="Usage Instructions!"
android:textAlignment="center" android:textAlignment="center"
android:textColor="#D0838383" android:textColor="#D0838383"
android:textSize="14sp" android:textSize="14sp"
android:gravity="center" app:layout_constraintBottom_toBottomOf="@+id/btn_Insta"
app:layout_constraintBottom_toBottomOf="@+id/developer_insta_spotify"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_linkedin" app:layout_constraintStart_toEndOf="@+id/btn_linkedin"
app:layout_constraintTop_toTopOf="@+id/btn_github_spotify" /> app:layout_constraintTop_toTopOf="@+id/btn_github" />
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_donate" android:id="@+id/btn_donate"
@ -188,6 +163,7 @@
android:background="@drawable/text_background_accented" android:background="@drawable/text_background_accented"
android:drawableEnd="@drawable/ic_mug" android:drawableEnd="@drawable/ic_mug"
android:drawablePadding="5dp" android:drawablePadding="5dp"
android:contentDescription="Donate Money Button"
android:fontFamily="@font/capriola" android:fontFamily="@font/capriola"
android:foreground="@drawable/rounded_gradient" android:foreground="@drawable/rounded_gradient"
android:gravity="end|center_vertical" android:gravity="end|center_vertical"
@ -212,40 +188,31 @@
<ImageButton <ImageButton
android:id="@+id/btn_github_spotify" android:id="@+id/btn_github"
android:layout_width="48dp" style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:layout_height="wrap_content"
android:background="@color/black"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_github" android:src="@drawable/ic_github"
app:layout_constraintBottom_toTopOf="@+id/btn_linkedin" app:layout_constraintBottom_toTopOf="@+id/btn_linkedin"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_youtube" app:layout_constraintTop_toBottomOf="@+id/btn_youtube"
android:contentDescription="Open Github App Button"
app:layout_constraintVertical_chainStyle="packed"/> app:layout_constraintVertical_chainStyle="packed"/>
<ImageButton <ImageButton
android:id="@+id/btn_linkedin" android:id="@+id/btn_linkedin"
android:layout_width="48dp" style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:layout_height="wrap_content"
android:background="@color/black"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_linkedin" android:src="@drawable/ic_linkedin"
app:layout_constraintBottom_toTopOf="@+id/developer_insta_spotify" app:layout_constraintBottom_toTopOf="@+id/btn_Insta"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_github_spotify" /> android:contentDescription="Open LinkedIN App Button"
app:layout_constraintTop_toBottomOf="@+id/btn_github"/>
<ImageButton <ImageButton
android:id="@+id/developer_insta_spotify" android:id="@+id/btn_Insta"
android:layout_width="48dp" style="@style/Widget.AppCompat.ImageButton.platformIcon"
android:layout_height="wrap_content"
android:background="@color/black"
android:padding="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_instagram" android:src="@drawable/ic_instagram"
app:layout_constraintBottom_toTopOf="@+id/btn_donate" app:layout_constraintBottom_toTopOf="@+id/btn_donate"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:contentDescription="Open Instagram App Button"
app:layout_constraintTop_toBottomOf="@+id/btn_linkedin" /> app:layout_constraintTop_toBottomOf="@+id/btn_linkedin" />
<TextView <TextView
@ -289,4 +256,3 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>
</layout>

View File

@ -16,19 +16,20 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_youtube" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="25dp"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:paddingTop="16dp"
tools:context=".ui.spotify.SpotifyFragment"> tools:context=".ui.spotify.SpotifyFragment">
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_download_all_spotify" android:id="@+id/btn_download_all"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="44dp" android:layout_height="44dp"
android:background="@drawable/btn_design" android:background="@drawable/btn_design"
@ -39,19 +40,18 @@
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="16sp" android:textSize="16sp"
android:visibility="visible" android:visibility="visible"
app:layout_anchor="@+id/appbar_spotify" app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center" /> app:layout_anchorGravity="bottom|center" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/downloading_fab_spotify" android:id="@+id/downloading_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:keepScreenOn="true"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:backgroundTint="@color/black" android:backgroundTint="@color/black"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:visibility="gone" android:visibility="gone"
app:borderWidth="0dp" app:borderWidth="0dp"
app:layout_anchor="@+id/appbar_spotify" app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center" app:layout_anchorGravity="bottom|center"
app:maxImageSize="38dp" app:maxImageSize="38dp"
app:rippleColor="@color/colorPrimaryDark" app:rippleColor="@color/colorPrimaryDark"
@ -59,7 +59,7 @@
app:tint="@null" /> app:tint="@null" />
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_spotify" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="280dp"> android:layout_height="280dp">
@ -72,14 +72,14 @@
app:toolbarId="@+id/toolbar"> app:toolbarId="@+id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/TopLayout_spotify" android:id="@+id/topLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:background="@color/black"
android:foreground="@drawable/gradient" android:foreground="@drawable/gradient"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ImageView <ImageView
android:id="@+id/spotify_cover_image" android:id="@+id/cover_image"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="28dp" android:layout_marginTop="28dp"
@ -89,13 +89,13 @@
android:src="@drawable/spotify_download" android:src="@drawable/spotify_download"
android:visibility="visible" android:visibility="visible"
app:layout_collapseMode="parallax" app:layout_collapseMode="parallax"
app:layout_constraintBottom_toTopOf="@id/title_view_spotify" app:layout_constraintBottom_toTopOf="@id/title_view"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/StatusBar_spotify" android:id="@+id/statusBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
@ -113,11 +113,11 @@
android:textColor="@color/grey" android:textColor="@color/grey"
android:textSize="16sp" android:textSize="16sp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/spotify_cover_image" app:layout_constraintBottom_toTopOf="@+id/cover_image"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
android:id="@+id/title_view_spotify" android:id="@+id/title_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
@ -142,7 +142,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_list_spotify" android:id="@+id/track_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingTop="26dp" android:paddingTop="26dp"
@ -152,16 +152,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_spotify" /> app:layout_constraintTop_toBottomOf="@id/appbar" />
<WebView
android:id="@+id/webView_spotify"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_gravity="bottom"
android:visibility="gone"
app:layout_anchorGravity="bottom"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -16,22 +16,12 @@
~ You should have received a copy of the GNU General Public License ~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="track"
type="com.shabinder.spotiflyer.models.Track" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="80dp"
android:background="#000000"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
> android:background="#000000">
<ImageView <ImageView
android:id="@+id/imageUrl" android:id="@+id/imageUrl"
@ -107,6 +97,3 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@ -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>

View File

@ -22,15 +22,6 @@
android:id="@+id/navigation" android:id="@+id/navigation"
app:startDestination="@id/mainFragment"> app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/spotifyFragment"
android:name="com.shabinder.spotiflyer.ui.spotify.SpotifyFragment"
android:label="main_fragment"
tools:layout="@layout/spotify_fragment" >
<argument
android:name="link"
app:argType="string" />
</fragment>
<fragment <fragment
android:id="@+id/mainFragment" android:id="@+id/mainFragment"
android:name="com.shabinder.spotiflyer.ui.mainfragment.MainFragment" android:name="com.shabinder.spotiflyer.ui.mainfragment.MainFragment"
@ -51,16 +42,11 @@
app:destination="@id/downloadRecord" app:destination="@id/downloadRecord"
app:enterAnim="@android:anim/slide_in_left" app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right" /> app:exitAnim="@android:anim/slide_out_right" />
<action
android:id="@+id/action_mainFragment_to_gaanaFragment"
app:destination="@id/gaanaFragment" />
</fragment> </fragment>
<fragment
android:id="@+id/youtubeFragment"
android:name="com.shabinder.spotiflyer.ui.youtube.YoutubeFragment"
android:label="YoutubeFragment"
tools:layout="@layout/youtube_fragment">
<argument
android:name="link"
app:argType="string" />
</fragment>
<fragment <fragment
android:id="@+id/downloadRecord" android:id="@+id/downloadRecord"
android:name="com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragment" android:name="com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragment"
@ -76,5 +62,35 @@
app:destination="@id/youtubeFragment" app:destination="@id/youtubeFragment"
app:enterAnim="@android:anim/slide_in_left" app:enterAnim="@android:anim/slide_in_left"
app:exitAnim="@android:anim/slide_out_right"/> app:exitAnim="@android:anim/slide_out_right"/>
<action
android:id="@+id/action_downloadRecord_to_gaanaFragment"
app:destination="@id/gaanaFragment" />
</fragment>
<fragment
android:id="@+id/spotifyFragment"
android:name="com.shabinder.spotiflyer.ui.spotify.SpotifyFragment"
android:label="main_fragment"
tools:layout="@layout/track_list_fragment" >
<argument
android:name="link"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/youtubeFragment"
android:name="com.shabinder.spotiflyer.ui.youtube.YoutubeFragment"
android:label="YoutubeFragment"
tools:layout="@layout/track_list_fragment">
<argument
android:name="link"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/gaanaFragment"
android:name="com.shabinder.spotiflyer.ui.gaana.GaanaFragment"
android:label="GaanaFragment"
tools:layout="@layout/track_list_fragment">
<argument
android:name="link"
app:argType="string" />
</fragment> </fragment>
</navigation> </navigation>

View File

@ -20,10 +20,12 @@
<resources> <resources>
<color name="colorPrimary">#FC5C7D</color> <color name="colorPrimary">#FC5C7D</color>
<color name="colorPrimaryDark">#CE1CFF</color> <color name="colorPrimaryDark">#CE1CFF</color>
<color name="colorAccent">#799BFF</color> <color name="colorAccent">#9AB3FF</color>
<color name="white">#FFFFFF</color> <color name="white">#FFFFFF</color>
<color name="grey">#99FFFFFF</color> <color name="grey">#99FFFFFF</color>
<color name="black">#000000</color> <color name="black">#000000</color>
<color name="dark">#121212</color>
<color name="successGreen">#59C351</color>
<color name="errorRed">#FF9494</color>
</resources> </resources>

View File

@ -19,37 +19,89 @@
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
<item name="colorPrimaryDark">#000000</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="android:colorBackground">@color/black</item>
<item name="colorOnBackground">@color/white</item>
<item name="colorError">#FF5E5E</item>
<item name="colorOnError">@color/black</item>
<item name="colorSurface">@color/dark</item>
<item name="statusBarScrim">@color/black</item>
<item name="android:statusBarColor">@color/black</item>
<item name="colorOnSurface">@color/white</item>
<item name="colorPrimary">#FC5C7D</item> <item name="colorPrimary">#FC5C7D</item>
<item name="android:background">#000000</item> <item name="colorOnPrimary">@color/white</item>
<item name="colorSecondary">@color/colorPrimaryDark</item>
<item name="colorOnSecondary">@color/white</item>
<item name="android:textColor">#FFFFFF</item> <item name="android:textColor">#FFFFFF</item>
<item name="colorAccent">#6A82FB</item> <item name="android:textColorHint">@color/grey</item>
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">#A9B200FF</item> <item name="colorAccent">@color/colorAccent</item>
<item name="android:radius">11dp</item> <item name="android:outlineAmbientShadowColor" tools:targetApi="p">@color/colorPrimaryDark</item>
<item name="android:radius">12dp</item>
<!-- Text Appearances !--> <!-- Text Appearances !-->
<!-- use our brand's custom TextAppearance4 !--> <!-- use our brand's custom TextAppearance4 !-->
<item name="textAppearanceHeadline4">@style/TextAppearance.AppTheme.Headline4</item> <item name="textAppearanceHeadline4">@style/TextAppearance.AppTheme.Headline4</item>
<!-- use default Body2 text apperance !--> <!-- use default Body2 text appearance !-->
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item> <item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
</style> </style>
<style name="Widget.AppCompat.Button.Colored.Gradient" parent="Widget.AppCompat.ActionButton">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@drawable/btn_design</item>
<item name="android:textColor">@color/black</item>
<item name="android:textAllCaps">true</item>
<item name="android:textAlignment">center</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:textSize">16sp</item>
</style>
<style name="Widget.AppCompat.ImageButton.40dp" parent="Widget.AppCompat.ImageButton">
<item name="android:layout_width">40dp</item>
<item name="android:layout_height">40dp</item>
<item name="android:background">@drawable/transparent</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:layout_margin">8dp</item>
</style>
<style name="Widget.AppCompat.TextView.Gradient">
<item name="android:background">@drawable/text_background_accented</item>
<item name="android:textAlignment">center</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:inputType">text</item>
<item name="android:gravity">center</item>
<item name="android:layout_gravity">center</item>
<item name="android:padding">8dp</item>
<item name="android:textSize">18sp</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="Widget.AppCompat.ImageButton.platformIcon" parent="Widget.AppCompat.ImageButton">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:background">@color/black</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:padding">4dp</item>
</style>
<style name="AlertDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog.Alert"> <style name="AlertDialogTheme" parent="ThemeOverlay.MaterialComponents.Dialog.Alert">
<item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item> <item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item>
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item> <item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item> <item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
<item name="android:textSize">22sp</item>
<item name="fontFamily">@font/amita</item>
</style> </style>
<style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent"> <style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="background">@color/white</item> <item name="background">@color/white</item>
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">20dp</item> <item name="cornerSize">8dp</item>
</style> </style>
<style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton"> <style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton">
<item name="backgroundTint">@color/colorPrimary</item> <item name="backgroundTint">@color/colorPrimary</item>
<item name="rippleColor">@color/colorPrimaryDark</item> <item name="rippleColor">@color/cardview_dark_background</item>
<item name="android:textColor">@android:color/black</item> <item name="android:textColor">@android:color/black</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:layout_marginEnd">4dp</item>
<item name="android:layout_marginBottom">2dp</item>
<item name="android:textAllCaps">false</item> <item name="android:textAllCaps">false</item>
</style> </style>

View File

@ -18,8 +18,8 @@
<AppUpdater> <AppUpdater>
<update> <update>
<latestVersion>1.5.1</latestVersion> <latestVersion>1.6</latestVersion>
<latestVersionCode>7</latestVersionCode> <latestVersionCode>8</latestVersionCode>
<url>https://github.com/Shabinder/SpotiFlyer/releases/download/1.5/SpotiFlyer-v1.5.apk</url> <url>https://github.com/Shabinder/SpotiFlyer/releases/</url>
</update> </update>
</AppUpdater> </AppUpdater>

View File

@ -20,7 +20,7 @@ buildscript {
ext{ ext{
kotlin_version = "1.4.10" kotlin_version = "1.4.10"
navigationVersion = '2.3.0' navigationVersion = '2.3.0'
ext.hilt_version = '2.28-alpha' ext.hilt_version = '2.29.1-alpha'
} }
repositories { repositories {
google() google()
@ -33,7 +33,7 @@ buildscript {
//safe-Args //safe-Args
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }