Latest FFMPEG Async Tasks,Mp3 Art and Album Data,Fetch Downloader Implemented.

This commit is contained in:
shabinder 2020-07-30 12:53:56 +05:30
parent ac683279e6
commit a802751942
38 changed files with 1201 additions and 205 deletions

View File

@ -1,7 +1,10 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="shabinder"> <dictionary name="shabinder">
<words> <words>
<w>ffmpeg</w>
<w>flyer</w> <w>flyer</w>
<w>insta</w>
<w>instagram</w>
<w>moshi</w> <w>moshi</w>
<w>musicforeveryone</w> <w>musicforeveryone</w>
<w>musicplaceholder</w> <w>musicplaceholder</w>

View File

@ -31,5 +31,10 @@
<option name="name" value="maven" /> <option name="name" value="maven" />
<option name="url" value="https://jitpack.io" /> <option name="url" value="https://jitpack.io" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://dl.bintray.com/hummatli/maven/" />
</remote-repository>
</component> </component>
</project> </project>

View File

@ -20,7 +20,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' 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: 'kotlinx-serialization'
android { android {
compileSdkVersion 29 compileSdkVersion 29
@ -28,16 +28,14 @@ android {
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 1 versionCode 2
versionName "1.0" versionName "1.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -51,15 +49,20 @@ android {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
} }
lintOptions {
abortOnError false
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
} }
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
@ -72,17 +75,21 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
// implementation "androidx.room:room-runtime:2.2.5" implementation "androidx.room:room-runtime:2.2.5"
// 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.github.bumptech.glide:glide:4.11.0" implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
kapt "com.github.bumptech.glide:compiler:4.11.0" transitive = true
}
kapt ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = true
}
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0' implementation 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'
implementation 'com.google.oauth-client:google-oauth-client:1.22.0' implementation 'com.google.oauth-client:google-oauth-client:1.22.0'
implementation 'com.spotify.android:auth:1.1.0' // implementation 'com.spotify.android:auth:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.8.0' implementation 'com.squareup.okhttp3:okhttp:4.8.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
@ -90,8 +97,16 @@ dependencies {
implementation "com.squareup.moshi:moshi-kotlin:1.9.3" implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
implementation "com.squareup.retrofit2:converter-moshi:2.9.0" implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" // JVM dependency
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'com.arthenica:mobile-ffmpeg-audio:4.4.LTS'
implementation 'com.shreyaspatil:EasyUpiPayment:2.2' implementation 'com.shreyaspatil:EasyUpiPayment:2.2'
implementation 'com.github.sealedtx:java-youtube-downloader:2.2.2' implementation 'com.github.sealedtx:java-youtube-downloader:2.2.3'
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.4"
implementation 'com.github.javiersantos:AppUpdater:2.7'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'

View File

@ -26,9 +26,12 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />--> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />-->
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -48,20 +51,21 @@
<activity android:name="com.shabinder.spotiflyer.splash.SplashScreen" <activity android:name="com.shabinder.spotiflyer.splash.SplashScreen"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Transparent"> android:theme="@style/Theme.Transparent">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <!-- <activity
android:name="com.spotify.sdk.android.authentication.LoginActivity" android:name="com.spotify.sdk.android.authentication.LoginActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
-->
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" /> android:resource="@array/preloaded_fonts" />
<service android:name=".worker.ForegroundService"/>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,63 @@
/*
* 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
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
/*
* 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/>.
*/
class App:Application() {
private val channelId = "ForegroundServiceChannel"
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
channelId,
"ForeGround Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(serviceChannel)
}
}
}

View File

@ -18,23 +18,26 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import android.Manifest import android.Manifest
import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.github.javiersantos.appupdater.AppUpdater
import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.databinding.MainActivityBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.SpotifyService
import com.shabinder.spotiflyer.utils.SpotifyServiceToken import com.shabinder.spotiflyer.utils.SpotifyServiceToken
import com.shabinder.spotiflyer.utils.YoutubeInterface import com.shabinder.spotiflyer.utils.YoutubeInterface
import com.shabinder.spotiflyer.utils.createDirectory
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
@ -48,12 +51,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class MainActivity : AppCompatActivity() ,DownloadHelper{ class MainActivity : AppCompatActivity(){
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private var ytDownloader : YoutubeDownloader? = null private var ytDownloader : YoutubeDownloader? = null
private var spotifyService : SpotifyService? = null private var spotifyService : SpotifyService? = null
private var spotifyServiceToken : SpotifyServiceToken? = null private var spotifyServiceToken : SpotifyServiceToken? = null
private var downloadManager : DownloadManager? = null
// private val redirectUri = "spotiflyer://callback" // private val redirectUri = "spotiflyer://callback"
private val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d" private val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d"
private val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8" private val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8"
@ -63,20 +65,21 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
private var token :String ="" private var token :String =""
private lateinit var sharedViewModel: SharedViewModel private lateinit var sharedViewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this,R.layout.main_activity) binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
sharedPref = this.getPreferences(Context.MODE_PRIVATE) sharedPref = this.getPreferences(Context.MODE_PRIVATE)
// if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ /* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){
// val savedToken = sharedPref?.getString("token","error")!! val savedToken = sharedPref?.getString("token","error")!!
// sharedViewModel.accessToken.value = savedToken sharedViewModel.accessToken.value = savedToken
// Log.i("SharedPrefs Token:",savedToken) Log.i("SharedPrefs Token:",savedToken)
// token = savedToken token = savedToken
//
// implementSpotifyService(savedToken) implementSpotifyService(savedToken)
// }else{authenticateSpotify()} }else{authenticateSpotify()}*/
if(sharedViewModel.spotifyService == null){ if(sharedViewModel.spotifyService == null){
authenticateSpotify() authenticateSpotify()
@ -85,6 +88,12 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
} }
requestPermission() requestPermission()
checkIfLatestVersion()
createDir()
setUpi()
isConnected = isOnline()
sharedViewModel.isConnected.value = isConnected
Log.i("Connection Status",isConnected.toString())
//Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"} //Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"}
ytDownloader = YoutubeDownloader() ytDownloader = YoutubeDownloader()
@ -92,32 +101,9 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
//Initialing Communication with Youtube //Initialing Communication with Youtube
YoutubeInterface.youtubeConnector() YoutubeInterface.youtubeConnector()
//Getting System Download Manager
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
sharedViewModel.downloadManager = downloadManager
isConnected = isOnline()
sharedViewModel.isConnected.value = isConnected
Log.i("Connection Status",isConnected.toString())
easyUpiPayment = EasyUpiPayment.Builder()
.with(this)
.setPayeeVpa("technoshab@paytm")
.setPayeeName("Shabinder Singh")
.setTransactionId("UNIQUE_TRANSACTION_ID")
.setTransactionRefId("UNIQUE_TRANSACTION_REF_ID")
.setDescription("Thanks for donating")
.setAmount("39.00")
.build()
sharedViewModel.easyUpiPayment = easyUpiPayment
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
} }
/** /**
* Adding my own new Spotify Web Api Requests! * Adding my own new Spotify Web Api Requests!
* */ * */
@ -246,6 +232,48 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
} }
} }
private fun setUpi() {
easyUpiPayment = EasyUpiPayment.Builder()
.with(this)
.setPayeeVpa("technoshab@paytm")
.setPayeeName("Shabinder Singh")
.setTransactionId("UNIQUE_TRANSACTION_ID")
.setTransactionRefId("UNIQUE_TRANSACTION_REF_ID")
.setDescription("Thanks for donating")
.setAmount("39.00")
.build()
sharedViewModel.easyUpiPayment = easyUpiPayment
}
private fun createDir() {
createDirectory(DownloadHelper.defaultDir)
createDirectory(DownloadHelper.defaultDir+".Images/")
createDirectory(DownloadHelper.defaultDir+"Tracks/")
createDirectory(DownloadHelper.defaultDir+"Albums/")
createDirectory(DownloadHelper.defaultDir+"Playlists/")
}
private fun checkIfLatestVersion() {
val appUpdater = AppUpdater(this)
.showAppUpdated(false)//true:Show App is Update Dialog
.setUpdateFrom(UpdateFrom.XML)
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
.setCancelable(false)
.setButtonUpdateClickListener { _, _ ->
val uri: Uri =
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
.setButtonDismissClickListener { dialog, _ ->
dialog.dismiss()
}
appUpdater.start()
}
/* /*
private fun authenticateSpotify() { private fun authenticateSpotify() {
val builder = AuthenticationRequest.Builder(clientId,AuthenticationResponse.Type.TOKEN,redirectUri) val builder = AuthenticationRequest.Builder(clientId,AuthenticationResponse.Type.TOKEN,redirectUri)

View File

@ -17,9 +17,9 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.Environment
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
@ -32,15 +32,16 @@ import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
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 = "" var intentString = ""
var accessToken = MutableLiveData<String>().apply { value = "" } var accessToken = MutableLiveData<String>().apply { value = "" }
var spotifyService : SpotifyService? = null var spotifyService : SpotifyService? = null
var ytDownloader : YoutubeDownloader? = null var ytDownloader : YoutubeDownloader? = null
var downloadManager : DownloadManager? = null
var isConnected = MutableLiveData<Boolean>().apply { value = false } var isConnected = MutableLiveData<Boolean>().apply { value = false }
var easyUpiPayment: EasyUpiPayment? = null var easyUpiPayment: EasyUpiPayment? = null
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator
private var viewModelJob = Job() private var viewModelJob = Job()
@ -64,13 +65,12 @@ class SharedViewModel : ViewModel() {
} }
fun showAlertDialog(resources:Resources,context: Context){ fun showAlertDialog(resources:Resources,context: Context){
val dialog = MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme) MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title)) .setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text)) .setMessage(resources.getString(R.string.supporting_text))
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ -> .setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
// Respond to neutral button press // Respond to neutral button press
} }
.setBackground(resources.getDrawable(R.drawable.gradient))
.show() .show()
} }
} }

View File

@ -17,22 +17,28 @@
package com.shabinder.spotiflyer.downloadHelper package com.shabinder.spotiflyer.downloadHelper
import android.app.DownloadManager import android.content.Context
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED import android.content.Intent
import android.net.Uri
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
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.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.spotiflyer.fragments.MainFragment import com.shabinder.spotiflyer.fragments.MainFragment
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.Track
import com.shabinder.spotiflyer.utils.YoutubeInterface import com.shabinder.spotiflyer.utils.YoutubeInterface
import com.shabinder.spotiflyer.worker.ForegroundService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
interface DownloadHelper { object DownloadHelper {
var context : Context? = null
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
private var downloadList = arrayListOf<DownloadObject>()
/** /**
* Function To Download All Tracks Available in a List * Function To Download All Tracks Available in a List
@ -40,111 +46,106 @@ interface DownloadHelper {
suspend fun downloadAllTracks( suspend fun downloadAllTracks(
type:String, type:String,
subFolder: String?, subFolder: String?,
trackList: List<Track>, ytDownloader: YoutubeDownloader?, downloadManager: DownloadManager?) { trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
trackList.forEach { downloadTrack(null,type,subFolder,ytDownloader,downloadManager,"${it.name} ${it.artists?.get(0)?.name ?:""}") } var size = trackList.size
trackList.forEach {
size--
if(size == 0){
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
}else{
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it )
}
}
} }
suspend fun downloadTrack( suspend fun downloadTrack(
mainFragment: MainFragment?, mainFragment: MainFragment?,
type:String, type:String,
subFolder:String?, subFolder:String?,
ytDownloader: YoutubeDownloader?, ytDownloader: YoutubeDownloader?,
downloadManager: DownloadManager?, searchQuery: String,
searchQuery: String track: Track,
index: Int? = null
) { ) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val data = YoutubeInterface.search(searchQuery)?.get(0) val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!!
if (data == null) {
Log.i("DownloadHelper", "Youtube Request Failed!")
} else {
val video = ytDownloader?.getVideo(data.id) //Fetching a Video Object.
//Fetching a Video Object. try {
val details = video?.details() val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data)
try{ withContext(Dispatchers.Main) {
val format: Format = mainFragment?.showToast("Starting Download")
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format }
val audioUrl = format.url() downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
Log.i("DHelper Link Found", audioUrl) } catch (e: java.lang.IndexOutOfBoundsException) {
if (audioUrl != null) { try {
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type) val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main) {
mainFragment?.showToast("Download Started") mainFragment?.showToast("Starting Download")
}
} else {
Log.i("YT audio url is null", format.toString())
} }
}catch (e:ArrayIndexOutOfBoundsException){ downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
try{ } catch (e: java.lang.IndexOutOfBoundsException) {
val format: Format = try {
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data)
val audioUrl = format.url() withContext(Dispatchers.Main) {
Log.i("DHelper Link Found", audioUrl) mainFragment?.showToast("Starting Download")
if (audioUrl != null) {
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
withContext(Dispatchers.Main){
mainFragment?.showToast("Download Started")
}
} else {
Log.i("YT audio url is null", format.toString())
}
}catch (e:ArrayIndexOutOfBoundsException){
try{
val format: Format =
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
val audioUrl = format.url()
Log.i("DHelper Link Found", audioUrl)
if (audioUrl != null) {
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
withContext(Dispatchers.Main){
mainFragment?.showToast("Download Started")
}
} else {
Log.i("YT audio url is null", format.toString())
}
}catch(e:ArrayIndexOutOfBoundsException){
Log.i("Catch",e.toString())
} }
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
} catch (e: java.lang.IndexOutOfBoundsException) {
Log.i("Catch", e.toString())
} }
} }
} }
} }
} }
/** private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String {
* Downloading Using Android Download Manager val video = ytDownloader?.getVideo(data.id)
* */ val format: Format =
suspend fun downloadFile(url: String, downloadManager: DownloadManager?, title: String,subFolder: String?,type: String) { video?.findAudioWithQuality(quality)?.get(0) as Format
Log.i("Format", video.findAudioWithQuality(AudioQuality.medium)?.get(0)!!.mimeType())
val audioUrl:String = format.url()
Log.i("DHelper Link Found", audioUrl)
return audioUrl
}
private suspend fun downloadFile(url: String, title: String, subFolder: String?, type: String, track:Track, index:Int? = null,mainFragment: MainFragment?) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val audioUri = Uri.parse(url) val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
val outputDir:String = DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
File.separator + "SpotiFlyer" + File.separator + type + File.separator + (if(subFolder == null){""}else{subFolder + File.separator}) + "${removeIllegalChars(title)}.mp3"
val request = DownloadManager.Request(audioUri) if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){
.setAllowedNetworkTypes( val downloadObject = DownloadObject(
DownloadManager.Request.NETWORK_WIFI or track = track,
DownloadManager.Request.NETWORK_MOBILE url = url,
outputDir = outputFile
) )
.setAllowedOverRoaming(false) Log.i("DH",outputFile)
.setTitle(title) if(index==null){
.setDescription("Spotify Downloader Working Up here...") downloadList.add(downloadObject)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir) }else{
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) downloadList.add(downloadObject)
downloadManager?.enqueue(request) startService(context!!, downloadList)
Log.i("DownloadManager", "Download Request Sent") downloadList = arrayListOf()
}
}else{withContext(Dispatchers.Main){mainFragment?.showToast("${track.name} is already Downloaded")}}
} }
} }
private fun startService(context:Context,list: ArrayList<DownloadObject>) {
val serviceIntent = Intent(context, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("list",list)
ContextCompat.startForegroundService(context, serviceIntent)
}
/** /**
* Removing Illegal Chars from File Name * Removing Illegal Chars from File Name
* **/ * **/
fun removeIllegalChars(fileName: String): String? { private fun removeIllegalChars(fileName: String): String? {
val illegalCharArray = charArrayOf( val illegalCharArray = charArrayOf(
'/', '/',
'\n', '\n',
@ -161,12 +162,14 @@ interface DownloadHelper {
'|', '|',
'\"', '\"',
'.', '.',
':' ':',
'-'
) )
var name = fileName var name = fileName
for (c in illegalCharArray) { for (c in illegalCharArray) {
name = fileName.replace(c, '_') name = fileName.replace(c, '_')
} }
name = name.replace("\\s".toRegex(), "_")
return name return name
} }
} }

View File

@ -23,31 +23,42 @@ import android.content.pm.PackageManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
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.widget.Toast import android.widget.Toast
import androidx.core.net.toUri
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
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.MainActivity 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.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.Track
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.SpotifyService
import com.shabinder.spotiflyer.utils.bindImage import com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.copyTo
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class MainFragment : Fragment(),DownloadHelper { class MainFragment : Fragment() {
private lateinit var binding:MainFragmentBinding private lateinit var binding:MainFragmentBinding
private lateinit var mainViewModel: MainViewModel private lateinit var mainViewModel: MainViewModel
private lateinit var sharedViewModel: SharedViewModel private lateinit var sharedViewModel: SharedViewModel
@ -56,12 +67,13 @@ class MainFragment : Fragment(),DownloadHelper {
private var type:String = "" private var type:String = ""
private var spotifyLink = "" private var spotifyLink = ""
private var i: Intent? = null private var i: Intent? = null
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.main_fragment,container,false) binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
DownloadHelper.context = requireContext()
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
spotifyService = sharedViewModel.spotifyService spotifyService = sharedViewModel.spotifyService
@ -73,13 +85,14 @@ class MainFragment : Fragment(),DownloadHelper {
binding.usage.text = spanStringBuilder binding.usage.text = spanStringBuilder
openSpotifyButton() openSpotifyButton()
openGithubButton()
openInstaButton()
binding.btnDonate.setOnClickListener { binding.btnDonate.setOnClickListener {
sharedViewModel.easyUpiPayment?.startPayment() sharedViewModel.easyUpiPayment?.startPayment()
} }
binding.btnSearch.setOnClickListener { binding.btnSearch.setOnClickListener {
sharedViewModel.isConnected.value = isOnline()
spotifyLink = binding.linkSearch.text.toString() spotifyLink = binding.linkSearch.text.toString()
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
@ -87,16 +100,15 @@ class MainFragment : Fragment(),DownloadHelper {
Log.i("Fragment", "$type : $link") Log.i("Fragment", "$type : $link")
if(sharedViewModel.spotifyService == null){ if(sharedViewModel.spotifyService == null && !isOnline()){
(activity as MainActivity).authenticateSpotify() (activity as MainActivity).authenticateSpotify()
} }
if (type == "Error" || link == "Error") { if (type == "Error" || link == "Error") {
showToast("Please Check Your Link!") showToast("Please Check Your Link!")
} else if(sharedViewModel.isConnected.value == false){ } else if(!isOnline()){
sharedViewModel.showAlertDialog(resources,requireContext()) sharedViewModel.showAlertDialog(resources,requireContext())
} } else {
else {
adapter = TrackListAdapter() adapter = TrackListAdapter()
binding.trackList.adapter = adapter binding.trackList.adapter = adapter
adapter.sharedViewModel = sharedViewModel adapter.sharedViewModel = sharedViewModel
@ -106,7 +118,9 @@ class MainFragment : Fragment(),DownloadHelper {
if(mainViewModel.searchLink == spotifyLink){ if(mainViewModel.searchLink == spotifyLink){
//it's a Device Configuration Change //it's a Device Configuration Change
adapterConfig(mainViewModel.trackList) adapterConfig(mainViewModel.trackList)
bindImage(binding.imageView,mainViewModel.coverUrl) sharedViewModel.uiScope.launch {
bindImage(binding.imageView,mainViewModel.coverUrl)
}
}else{ }else{
when (type) { when (type) {
"track" -> { "track" -> {
@ -121,15 +135,14 @@ class MainFragment : Fragment(),DownloadHelper {
adapterConfig(trackList) adapterConfig(trackList)
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
showToast("Starting Download in Few Seconds")
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadAllTracks( downloadAllTracks(
"Tracks", "Tracks",
null, null,
trackList, trackList,
sharedViewModel.ytDownloader, sharedViewModel.ytDownloader)
sharedViewModel.downloadManager
)
} }
} }
} }
@ -142,22 +155,21 @@ class MainFragment : Fragment(),DownloadHelper {
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
val albumObject = sharedViewModel.getAlbumDetails(link) val albumObject = sharedViewModel.getAlbumDetails(link)
val trackList = mutableListOf<Track>() val trackList = mutableListOf<Track>()
albumObject!!.tracks?.items?.forEach { trackList.add(it!!) } albumObject!!.tracks?.items?.forEach { trackList.add(it) }
mainViewModel.trackList = trackList mainViewModel.trackList = trackList
mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!! mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!!
bindImage(binding.imageView,mainViewModel.coverUrl) bindImage(binding.imageView,mainViewModel.coverUrl)
adapter.isAlbum = true adapter.isAlbum = true
adapterConfig(trackList) adapterConfig(trackList)
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
showToast("Starting Download in Few Seconds")
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
downloadAllTracks( downloadAllTracks(
"Albums", "Albums",
albumObject.name, albumObject.name,
trackList, trackList,
sharedViewModel.ytDownloader, sharedViewModel.ytDownloader)
sharedViewModel.downloadManager
)
} }
} }
} }
@ -171,21 +183,21 @@ class MainFragment : Fragment(),DownloadHelper {
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
val playlistObject = sharedViewModel.getPlaylistDetails(link) val playlistObject = sharedViewModel.getPlaylistDetails(link)
val trackList = mutableListOf<Track>() val trackList = mutableListOf<Track>()
playlistObject!!.tracks?.items!!.forEach { trackList.add(it?.track!!) } playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) }
mainViewModel.trackList = trackList mainViewModel.trackList = trackList
mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!! mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!!
bindImage(binding.imageView,mainViewModel.coverUrl) bindImage(binding.imageView,mainViewModel.coverUrl)
adapterConfig(trackList) adapterConfig(trackList)
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
showToast("Starting Download in Few Seconds")
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
loadAllImages(trackList)
downloadAllTracks( downloadAllTracks(
"Playlists", "Playlists",
playlistObject.name, playlistObject.name,
trackList, trackList,
sharedViewModel.ytDownloader, sharedViewModel.ytDownloader)
sharedViewModel.downloadManager
)
} }
} }
} }
@ -213,6 +225,59 @@ class MainFragment : Fragment(),DownloadHelper {
return binding.root return binding.root
} }
/**
* Function to fetch all Images for using in mp3 tag.
**/
private 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(),
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
)
resource?.copyTo(file)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return false
}
}).submit()
}
}
}
/**
* Implementing button to Open Spotify App
**/
private fun openSpotifyButton() { private fun openSpotifyButton() {
val manager: PackageManager = requireActivity().packageManager val manager: PackageManager = requireActivity().packageManager
try { try {
@ -232,14 +297,32 @@ class MainFragment : Fragment(),DownloadHelper {
} }
} }
private fun openGithubButton() {
val uri: Uri =
Uri.parse("http://github.com/Shabinder/SpotiFlyer")
val intent = Intent(Intent.ACTION_VIEW, uri)
binding.btnGithub.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.developerInsta.setOnClickListener {
startActivity(intent)
}
}
/** /**
* Configure Recycler View Adapter * Configure Recycler View Adapter
**/ **/
private fun adapterConfig(trackList: List<Track>){ private fun adapterConfig(trackList: List<Track>){
adapter.trackList = trackList.toList() adapter.trackList = trackList.toList()
adapter.totalItems = trackList.size adapter.totalItems = trackList.size
adapter.mainFragment = this
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
/** /**
@ -274,6 +357,10 @@ class MainFragment : Fragment(),DownloadHelper {
fun showToast(message:String){ fun showToast(message:String){
Toast.makeText(context,message,Toast.LENGTH_SHORT).show() Toast.makeText(context,message,Toast.LENGTH_SHORT).show()
} }
/**
* Util. Function To Check Connection Status
**/
private fun isOnline(): Boolean { private fun isOnline(): Boolean {
val cm = val cm =
requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

View File

@ -21,7 +21,7 @@ import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.Track
class MainViewModel: ViewModel() { class MainViewModel: ViewModel() {
var searchLink:String = "" var searchLink: String = ""
var trackList = mutableListOf<Track>() var trackList = mutableListOf<Track>()
var coverUrl:String = "" var coverUrl: String = ""
} }

View File

@ -17,6 +17,10 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Album( data class Album(
var album_type: String? = null, var album_type: String? = null,
var artists: List<Artist?>? = null, var artists: List<Artist?>? = null,
@ -33,6 +37,6 @@ data class Album(
var popularity: Int? = null, var popularity: Int? = null,
var release_date: String? = null, var release_date: String? = null,
var release_date_precision: String? = null, var release_date_precision: String? = null,
var tracks: PagingObject<Track?>? = null, var tracks: PagingObjectTrack? = null,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null):Parcelable

View File

@ -16,10 +16,15 @@
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Artist( data class Artist(
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 id: String? = null,
var name: String? = null, var name: String? = null,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null):Parcelable

View File

@ -16,6 +16,11 @@
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Copyright( data class Copyright(
var text: String? = null, var text: String? = null,
var type: String? = null) var type: String? = null):Parcelable

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
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class DownloadObject(
var track: Track,
var url:String,
var outputDir:String
):Parcelable

View File

@ -17,6 +17,10 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Episodes( data class Episodes(
var audio_preview_url:String?, var audio_preview_url:String?,
var description:String?, var description:String?,
@ -35,4 +39,4 @@ data class Episodes(
var release_date_precision:String?, var release_date_precision:String?,
var type:String?, var type:String?,
var uri:String var uri:String
) ): Parcelable

View File

@ -17,6 +17,9 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import kotlinx.serialization.Serializable
@Serializable
data class Followers( data class Followers(
var href: String? = null, var href: String? = null,
var total: Int? = null) var total: Int? = null):java.io.Serializable

View File

@ -16,7 +16,12 @@
*/ */
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Image( data class Image(
var width: Int? = null, var width: Int? = null,
var height: Int? = null, var height: Int? = null,
var url: String? = null) var url: String? = null):Parcelable

View File

@ -17,9 +17,13 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class LinkedTrack( data class LinkedTrack(
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 id: String? = null,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null): Parcelable

View File

@ -0,0 +1,31 @@
/*
* 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 android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class PagingObjectPlaylistTrack(
var href: String? = null,
var items: List<PlaylistTrack>? = null,
var limit: Int = 0,
var next: String? = null,
var offset: Int = 0,
var previous: String? = null,
var total: Int = 0): Parcelable

View File

@ -17,11 +17,15 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
data class PagingObject<T>( import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class PagingObjectTrack(
var href: String? = null, var href: String? = null,
var items: List<T>? = null, var items: List<Track>? = null,
var limit: Int = 0, var limit: Int = 0,
var next: String? = null, var next: String? = null,
var offset: Int = 0, var offset: Int = 0,
var previous: String? = null, var previous: String? = null,
var total: Int = 0) var total: Int = 0):Parcelable

View File

@ -17,7 +17,11 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import com.squareup.moshi.Json import com.squareup.moshi.Json
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Playlist( data class Playlist(
@Json(name = "collaborative")var is_collaborative: Boolean? = null, @Json(name = "collaborative")var is_collaborative: Boolean? = null,
var description: String? = null, var description: String? = null,
@ -30,6 +34,6 @@ data class Playlist(
var owner: UserPublic? = null, var owner: UserPublic? = null,
@Json(name = "public")var is_public: Boolean? = null, @Json(name = "public")var is_public: Boolean? = null,
var snapshot_id: String? = null, var snapshot_id: String? = null,
var tracks: PagingObject<PlaylistTrack?>? = null, var tracks: PagingObjectPlaylistTrack? = null,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null): Parcelable

View File

@ -17,8 +17,12 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class PlaylistTrack( data class PlaylistTrack(
var added_at: String? = null, var added_at: String? = null,
var added_by: UserPublic? = null, var added_by: UserPublic? = null,
var track: Track? = null, var track: Track? = null,
var is_local: Boolean? = null) var is_local: Boolean? = null): Parcelable

View File

@ -17,8 +17,12 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Token( data class Token(
var access_token:String, var access_token:String,
var token_type:String, var token_type:String,
var expires_in:Int var expires_in:Int
) ): Parcelable

View File

@ -17,6 +17,10 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Track( data class Track(
var artists: List<Artist?>? = null, var artists: List<Artist?>? = null,
var available_markets: List<String?>? = null, var available_markets: List<String?>? = null,
@ -35,4 +39,4 @@ data class Track(
var uri: String? = null, var uri: String? = null,
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):Parcelable

View File

@ -17,6 +17,10 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class UserPrivate( data class UserPrivate(
val country:String, val country:String,
var display_name: String, var display_name: String,
@ -28,4 +32,4 @@ data class UserPrivate(
var images: List<Image?>? = null, var images: List<Image?>? = null,
var product:String, var product:String,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null): Parcelable

View File

@ -17,6 +17,10 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class UserPublic( data class UserPublic(
var display_name: String? = null, var display_name: String? = null,
var external_urls: Map<String?, String?>? = null, var external_urls: Map<String?, String?>? = null,
@ -25,4 +29,4 @@ data class UserPublic(
var id: String? = null, var id: String? = null,
var images: List<Image?>? = null, var images: List<Image?>? = null,
var type: String? = null, var type: String? = null,
var uri: String? = null) var uri: String? = null): Parcelable

View File

@ -26,13 +26,14 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadTrack
import com.shabinder.spotiflyer.fragments.MainFragment import com.shabinder.spotiflyer.fragments.MainFragment
import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.models.Track
import com.shabinder.spotiflyer.utils.bindImage import com.shabinder.spotiflyer.utils.bindImage
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>(),DownloadHelper {
class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>() {
var trackList = listOf<Track>() var trackList = listOf<Track>()
var totalItems:Int = 0 var totalItems:Int = 0
@ -52,7 +53,9 @@ class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>(),Downl
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = trackList[position] val item = trackList[position]
if(totalItems == 1 || isAlbum){holder.coverImage.visibility = View.GONE}else{ if(totalItems == 1 || isAlbum){holder.coverImage.visibility = View.GONE}else{
bindImage(holder.coverImage, item.album!!.images?.get(0)?.url) sharedViewModel.uiScope.launch {
bindImage(holder.coverImage, item.album!!.images?.get(0)?.url)
}
} }
holder.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}" holder.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
@ -60,7 +63,7 @@ class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>(),Downl
holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec" holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
holder.downloadBtn.setOnClickListener{ holder.downloadBtn.setOnClickListener{
sharedViewModel.uiScope.launch { sharedViewModel.uiScope.launch {
downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,sharedViewModel.downloadManager,"${item.name} ${item.artists?.get(0)!!.name?:""}") downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0)
} }
} }

View File

@ -17,22 +17,102 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.databinding.BindingAdapter import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions 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.R
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.IOException
@BindingAdapter("imageUrl") @BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) { fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let { imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Glide.with(imgView.context) Glide
.with(imgView.context)
.asFile()
.load(imgUri) .load(imgUri)
.apply(RequestOptions() .placeholder(R.drawable.ic_song_placeholder)
.placeholder(R.drawable.ic_song_placeholder) .error(R.drawable.ic_musicplaceholder)
.error(R.drawable.ic_musicplaceholder)) .listener(object:RequestListener<File>{
.into(imgView) 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(),
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.ARGB_8888
val bitmap = BitmapFactory.decodeStream(FileInputStream(resource), null, options)
resource?.copyTo(file)
withContext(Dispatchers.Main){
imgView.setImageBitmap(bitmap)
// 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,343 @@
/*
* 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.worker
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Environment
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
import com.arthenica.mobileffmpeg.FFmpeg
import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.Track
import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock
import com.tonyodev.fetch2core.Func
import kotlinx.coroutines.*
import java.io.File
import java.io.FileInputStream
class ForegroundService : Service(){
private val tag = "Foreground Service"
private val channelId = "SpotiFlyer: Download Service"
private var total = 0 //Total Downloads Requested
private var converted = 0//Total Files Converted
private var fetch:Fetch? = null
private var downloadList = mutableListOf<DownloadObject>()
private var serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val requestMap = mutableMapOf<Request,Track>()
private val downloadMap = mutableMapOf<String,Track>()
private var speed :Long = 0
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("SpotiFlyer: Downloading Your Music")
.setSubText("Speed: $speed KB/s ")
.setNotificationSilent()
.setOnlyAlertOnce(true)
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
.setSmallIcon(R.drawable.down_arrowbw)
.build()
val fetchConfiguration =
FetchConfiguration.Builder(this)
.setDownloadConcurrentLimit(4)
.build()
Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration)
fetch = Fetch.getDefaultInstance()
fetch?.addListener(fetchListener)
startForeground(1, notification)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started
Log.i(tag,"Service Started.")
//do heavy work on a background thread
// val list = intent.getSerializableExtra("list") as List<Any?>
val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
Log.i(tag,"Intent List Size: ${list!!.size}")
total += list.size
list.forEach { downloadList.add(it as DownloadObject) }
serviceScope.launch {
withContext(Dispatchers.IO){
for (downloadObject in downloadList) {
val request= Request(downloadObject.url, downloadObject.outputDir)
request.priority = Priority.NORMAL
request.networkType = NetworkType.ALL
fetch?.enqueue(request,
Func {
Log.i("DownloadManager", "Download Request Sent")
requestMap[it] = downloadObject.track
downloadList.remove(downloadObject) },
Func {
Log.i("DownloadManager", "Download Request Error:${it.throwable.toString()}")}
)
}
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
if(downloadMap.isEmpty() && converted == total){
Log.i(tag,"Service destroyed.")
stopForeground(true)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(downloadMap.isEmpty() && converted == total ){
Log.i(tag,"Service destroyed.")
stopSelf()
}
}
private var fetchListener: FetchListener = object : FetchListener {
override fun onQueued(
download: Download,
waitingOnNetwork: Boolean
) {
// TODO("Not yet implemented")
}
override fun onRemoved(download: Download) {
// TODO("Not yet implemented")
}
override fun onResumed(download: Download) {
// TODO("Not yet implemented")
}
override fun onStarted(
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int
) {
val track = requestMap[download.request]
Log.i(tag,"${track?.name} Download Started")
}
override fun onWaitingNetwork(download: Download) {
// TODO("Not yet implemented")
}
override fun onAdded(download: Download) {
// TODO("Not yet implemented")
}
override fun onCancelled(download: Download) {
// TODO("Not yet implemented")
}
override fun onCompleted(download: Download) {
val track = requestMap[download.request]
speed = 0
serviceScope.launch {
convertToMp3(download.file, track!!)
}
Log.i(tag,"${track?.name} Download Completed")
requestMap.remove(download.request)
updateNotification()
}
override fun onDeleted(download: Download) {
// TODO("Not yet implemented")
}
override fun onDownloadBlockUpdated(
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int
) {
// TODO("Not yet implemented")
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
Log.i(tag,download.error.throwable.toString())
}
override fun onPaused(download: Download) {
// TODO("Not yet implemented")
}
override fun onProgress(
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
val track = requestMap[download.request]
Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec")
speed = (downloadedBytesPerSecond/1000)
updateNotification()
}
}
fun convertToMp3(filePath: String,track: Track){
val m4aFile = File(filePath)
val executionId = FFmpeg.executeAsync(
"-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
) { _, returnCode ->
when (returnCode) {
RETURN_CODE_SUCCESS -> {
Log.i(Config.TAG, "Async command execution completed successfully.")
m4aFile.delete()
writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track)
//FFMPEG task Completed
}
RETURN_CODE_CANCEL -> {
Log.i(Config.TAG, "Async command execution cancelled by user.")
}
else -> {
Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode))
}
}
}
}
private fun writeMp3Tags(filePath:String, track: Track){
var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File,track)
mp3File = setId3v2Tags(mp3File,track)
Log.i("Mp3Tags","saving file")
mp3File.save(filePath.substringBeforeLast('.')+".new.mp3")
val file = File(filePath)
file.delete()
val newFile = File((filePath.substringBeforeLast('.')+".new.mp3"))
newFile.renameTo(file)
converted++
updateNotification()
//All tasks completed (REST IN PEACE)
if(converted == total){
stopForeground(false)
stopSelf()
}
}
/**
* This is the method that can be called to update the Notification
*/
private fun updateNotification() {
val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("SpotiFlyer: Downloading Your Music")
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
.setSubText("Speed: $speed KB/s ")
.setNotificationSilent()
.setOnlyAlertOnce(true)
.setSmallIcon(R.drawable.down_arrowbw)
.build()
mNotificationManager.notify(1, notification)
}
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
val id3v1Tag = ID3v1Tag()
id3v1Tag.track = track.disc_number.toString()
val artistsList = mutableListOf<String>()
track.artists?.forEach { artistsList.add(it!!.name!!) }
id3v1Tag.artist = artistsList.joinToString()
id3v1Tag.title = track.name
id3v1Tag.album = track.album?.name
id3v1Tag.year = track.album?.release_date
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
mp3File.id3v1Tag = id3v1Tag
return mp3File
}
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
val id3v2Tag = ID3v24Tag()
id3v2Tag.track = track.disc_number.toString()
val artistsList = mutableListOf<String>()
track.artists?.forEach { artistsList.add(it!!.name!!) }
id3v2Tag.artist = artistsList.joinToString()
id3v2Tag.title = track.name
id3v2Tag.album = track.album?.name
id3v2Tag.year = track.album?.release_date
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
id3v2Tag.lyrics = "Gonna Implement Soon"
val copyrights = mutableListOf<String>()
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
id3v2Tag.copyright = copyrights.joinToString()
id3v2Tag.url = track.href
track.let {
val file = File(
Environment.getExternalStorageDirectory(),
DownloadHelper.defaultDir +".Images/" + (it.album!!.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
Log.i("Mp3Tags editing Tags",file.path)
//init array with file length
val bytesArray = ByteArray(file.length().toInt())
val fis = FileInputStream(file)
fis.read(bytesArray) //read file into bytes[]
fis.close()
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
}
id3v2Tag.albumImage
mp3file.id3v2Tag = id3v2Tag
return mp3file
}
private fun removeAllTags(mp3file: Mp3File): Mp3File {
if (mp3file.hasId3v1Tag()) {
mp3file.removeId3v1Tag()
}
if (mp3file.hasId3v2Tag()) {
mp3file.removeId3v2Tag()
}
if (mp3file.hasCustomTag()) {
mp3file.removeCustomTag()
}
return mp3file
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,21 @@
<!--
~ 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="38dp"
android:height="38dp" android:viewportWidth="512" android:viewportHeight="512">
<path android:fillColor="#5C6BC0" android:pathData="M255.968,5.329C114.624,5.329 0,120.401 0,262.353c0,113.536 73.344,209.856 175.104,243.872c12.8,2.368 17.472,-5.568 17.472,-12.384c0,-6.112 -0.224,-22.272 -0.352,-43.712c-71.2,15.52 -86.24,-34.464 -86.24,-34.464c-11.616,-29.696 -28.416,-37.6 -28.416,-37.6c-23.264,-15.936 1.728,-15.616 1.728,-15.616c25.696,1.824 39.2,26.496 39.2,26.496c22.848,39.264 59.936,27.936 74.528,21.344c2.304,-16.608 8.928,-27.936 16.256,-34.368c-56.832,-6.496 -116.608,-28.544 -116.608,-127.008c0,-28.064 9.984,-51.008 26.368,-68.992c-2.656,-6.496 -11.424,-32.64 2.496,-68c0,0 21.504,-6.912 70.4,26.336c20.416,-5.696 42.304,-8.544 64.096,-8.64c21.728,0.128 43.648,2.944 64.096,8.672c48.864,-33.248 70.336,-26.336 70.336,-26.336c13.952,35.392 5.184,61.504 2.56,68c16.416,17.984 26.304,40.928 26.304,68.992c0,98.72 -59.84,120.448 -116.864,126.816c9.184,7.936 17.376,23.616 17.376,47.584c0,34.368 -0.32,62.08 -0.32,70.496c0,6.88 4.608,14.88 17.6,12.352C438.72,472.145 512,375.857 512,262.353C512,120.401 397.376,5.329 255.968,5.329z"/>
</vector>

View File

@ -0,0 +1,25 @@
<!--
~ 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="25dp"
android:height="25dp" android:viewportWidth="512.007" android:viewportHeight="512.007">
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
<path android:fillColor="#FF000000" android:pathData="m237.72,453.517c-204.315,-181.786 -197.402,-175.776 -197.402,-175.776 -25.999,-24.984 -40.318,-58.201 -40.318,-93.533 0,-46.48 24.63,-91.702 65.906,-115.47 3.589,-2.067 8.174,-0.833 10.242,2.757 2.067,3.589 0.833,8.175 -2.757,10.242 -36.017,20.74 -58.391,60.004 -58.391,102.471 0,31.212 12.683,60.588 35.711,82.717 0,0 -6.881,-5.996 196.979,175.386 2.292,2.039 5.242,3.161 8.309,3.161 3.066,0 6.018,-1.123 8.31,-3.162l61.917,-55.089c3.095,-2.753 7.835,-2.477 10.588,0.618s2.477,7.835 -0.618,10.588l-61.917,55.09c-10.431,9.281 -26.148,9.263 -36.559,0zM357.363,377.059c-2.067,0 -4.124,-0.849 -5.606,-2.515 -2.753,-3.095 -2.477,-7.835 0.618,-10.588l105.273,-93.665c21.815,-19.409 35.132,-44.369 38.513,-72.181 0.001,-0.006 0.001,-0.012 0.002,-0.018 7.637,-62.927 -37.915,-131.557 -116.038,-131.557 -54.879,0 -102.877,38.923 -114.129,92.55 -1.005,4.79 -5.116,8.135 -9.997,8.135s-8.991,-3.346 -9.996,-8.136c-11.252,-53.626 -59.25,-92.549 -114.128,-92.549 -9.633,0 -19.082,1.076 -28.084,3.198 -4.033,0.952 -8.07,-1.548 -9.021,-5.579 -0.951,-4.032 1.547,-8.07 5.579,-9.021 10.128,-2.388 20.735,-3.598 31.525,-3.598 55.699,0 105.463,35.109 124.125,87.792 18.71,-52.817 68.567,-87.792 124.125,-87.792 84.905,0 139.884,74.56 130.929,148.362 0,0.007 -0.001,0.015 -0.002,0.022 -3.829,31.494 -18.847,59.703 -43.433,81.578l-105.273,93.665c-1.429,1.272 -3.209,1.897 -4.982,1.897z"/>
</vector>

View File

@ -0,0 +1,51 @@
<!--
~ 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" xmlns:aapt="http://schemas.android.com/aapt"
android:width="32dp" android:height="32dp"
android:viewportWidth="512" android:viewportHeight="512">
<path android:pathData="M352,0H160C71.648,0 0,71.648 0,160v192c0,88.352 71.648,160 160,160h192c88.352,0 160,-71.648 160,-160V160C512,71.648 440.352,0 352,0zM464,352c0,61.76 -50.24,112 -112,112H160c-61.76,0 -112,-50.24 -112,-112V160C48,98.24 98.24,48 160,48h192c61.76,0 112,50.24 112,112V352z">
<aapt:attr name="android:fillColor">
<gradient android:endX="465.1312" android:endY="46.8656"
android:startX="46.8688" android:startY="465.1344" android:type="linear">
<item android:color="#FFFFC107" android:offset="0"/>
<item android:color="#FFF44336" android:offset="0.507"/>
<item android:color="#FF9C27B0" android:offset="0.99"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M256,128c-70.688,0 -128,57.312 -128,128s57.312,128 128,128s128,-57.312 128,-128S326.688,128 256,128zM256,336c-44.096,0 -80,-35.904 -80,-80c0,-44.128 35.904,-80 80,-80s80,35.872 80,80C336,300.096 300.096,336 256,336z">
<aapt:attr name="android:fillColor">
<gradient android:endX="346.5072" android:endY="165.4928"
android:startX="165.4928" android:startY="346.5072" android:type="linear">
<item android:color="#FFFFC107" android:offset="0"/>
<item android:color="#FFF44336" android:offset="0.507"/>
<item android:color="#FF9C27B0" android:offset="0.99"/>
</gradient>
</aapt:attr>
</path>
<path android:pathData="M393.6,118.4m-17.056,0a17.056,17.056 0,1 1,34.112 0a17.056,17.056 0,1 1,-34.112 0">
<aapt:attr name="android:fillColor">
<gradient android:endX="405.6592" android:endY="106.3408"
android:startX="381.5408" android:startY="130.4624" android:type="linear">
<item android:color="#FFFFC107" android:offset="0"/>
<item android:color="#FFF44336" android:offset="0.507"/>
<item android:color="#FF9C27B0" android:offset="0.99"/>
</gradient>
</aapt:attr>
</path>
</vector>

View File

@ -1,5 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp" <!--
android:height="42dp" android:viewportWidth="512" android:viewportHeight="512"> ~ 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="40dp"
android:height="40dp" android:viewportWidth="512" android:viewportHeight="512">
<path android:fillColor="#ff5d7d" android:fillType="evenOdd" android:pathData="m258.229,255.863c-11.191,-11.155 -29.503,-11.155 -40.693,0 -4.486,4.471 -11.053,4.007 -15.072,0 -11.191,-11.155 -29.503,-11.155 -40.693,0 -30.403,30.307 28.128,83.271 48.229,88.64 20.102,-5.369 78.632,-58.333 48.229,-88.64z"/> <path android:fillColor="#ff5d7d" android:fillType="evenOdd" android:pathData="m258.229,255.863c-11.191,-11.155 -29.503,-11.155 -40.693,0 -4.486,4.471 -11.053,4.007 -15.072,0 -11.191,-11.155 -29.503,-11.155 -40.693,0 -30.403,30.307 28.128,83.271 48.229,88.64 20.102,-5.369 78.632,-58.333 48.229,-88.64z"/>
<path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="m258.229,255.863c30.403,30.307 -28.128,83.271 -48.23,88.64 -20.102,-5.369 -78.633,-58.334 -48.229,-88.64 11.191,-11.155 29.502,-11.155 40.693,0 4.02,4.007 10.587,4.471 15.072,0 11.191,-11.155 29.503,-11.155 40.694,0zM10,176c0,94.167 60,173.334 80,260h240c3.112,-13.487 7.193,-26.792 11.866,-40 4.742,-13.403 10.093,-26.707 15.66,-40 16.471,-39.33 34.83,-78.563 44.877,-119.994 3.154,-13.009 5.489,-26.235 6.689,-39.749 0.593,-6.679 0.908,-13.429 0.908,-20.257 0,-11 -9,-20 -20,-20 -120,0 -240,0 -360.001,0 -10.999,0 -19.999,9 -19.999,20z"/> <path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="m258.229,255.863c30.403,30.307 -28.128,83.271 -48.23,88.64 -20.102,-5.369 -78.633,-58.334 -48.229,-88.64 11.191,-11.155 29.502,-11.155 40.693,0 4.02,4.007 10.587,4.471 15.072,0 11.191,-11.155 29.503,-11.155 40.694,0zM10,176c0,94.167 60,173.334 80,260h240c3.112,-13.487 7.193,-26.792 11.866,-40 4.742,-13.403 10.093,-26.707 15.66,-40 16.471,-39.33 34.83,-78.563 44.877,-119.994 3.154,-13.009 5.489,-26.235 6.689,-39.749 0.593,-6.679 0.908,-13.429 0.908,-20.257 0,-11 -9,-20 -20,-20 -120,0 -240,0 -360.001,0 -10.999,0 -19.999,9 -19.999,20z"/>
<path android:fillColor="#ccf5fc" android:fillType="evenOdd" android:pathData="m402,356h-44.474c-5.567,13.293 -10.918,26.597 -15.66,40h60.134c55,0 99.999,-45 99.999,-100 0,-52.616 -41.185,-96.074 -92.908,-99.743 -1.2,13.514 -3.534,26.74 -6.69,39.749 32.818,0.218 59.599,27.129 59.599,59.994 0,33 -27,60 -60,60z"/> <path android:fillColor="#ccf5fc" android:fillType="evenOdd" android:pathData="m402,356h-44.474c-5.567,13.293 -10.918,26.597 -15.66,40h60.134c55,0 99.999,-45 99.999,-100 0,-52.616 -41.185,-96.074 -92.908,-99.743 -1.2,13.514 -3.534,26.74 -6.69,39.749 32.818,0.218 59.599,27.129 59.599,59.994 0,33 -27,60 -60,60z"/>

View File

@ -134,7 +134,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraint_layout" android:id="@+id/constraint_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:visibility="visible"> android:visibility="visible">
<TextView <TextView
@ -240,23 +240,95 @@
app:layout_constraintStart_toStartOf="@+id/btn_donate" app:layout_constraintStart_toStartOf="@+id/btn_donate"
app:layout_constraintTop_toBottomOf="@+id/usage" /> app:layout_constraintTop_toBottomOf="@+id/usage" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_github"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginTop="130dp"
android:background="@drawable/text_background"
android:drawableStart="@drawable/ic_github"
android:drawablePadding="5dp"
android:fontFamily="@font/capriola"
android:gravity="end|center_vertical"
android:padding="5dp"
android:text=" Github "
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_donate" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/developer_insta"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:layout_marginTop="130dp"
android:background="@drawable/text_background"
android:drawableEnd="@drawable/ic_instagram"
android:drawablePadding="5dp"
android:fontFamily="@font/capriola"
android:gravity="end|center_vertical"
android:padding="5dp"
android:text=" Follow Me "
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_donate" />
<TextView
android:id="@+id/tagLine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:layout_marginEnd="8dp"
android:fontFamily="@font/raleway_semibold"
android:text="Made with "
android:textAlignment="center"
android:textColor="@color/colorPrimary"
android:textSize="22sp"
app:layout_constraintEnd_toStartOf="@id/heart"
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
<ImageView
android:id="@+id/heart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="80dp"
android:contentDescription="Made With Love In India"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/developer_insta"
app:srcCompat="@drawable/ic_heart" />
<TextView
android:id="@+id/tagLine2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="80dp"
android:fontFamily="@font/raleway_semibold"
android:text=" in India"
android:textAlignment="center"
android:textColor="@color/colorPrimary"
android:textSize="22sp"
app:layout_constraintStart_toEndOf="@id/heart"
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_list" 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"
android:visibility="gone" android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/open_spotify" /> app:layout_constraintTop_toBottomOf="@id/open_spotify" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout> </layout>

View File

@ -1,5 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?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" <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">
@ -15,7 +32,6 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="80dp" android:layout_height="80dp"
android:contentDescription="Track Image" android:contentDescription="Track Image"
android:visibility="visible"
android:scaleType="centerInside" android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/artist" app:layout_constraintEnd_toStartOf="@+id/artist"

View File

@ -0,0 +1,25 @@
<?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/>.
-->
<AppUpdater>
<update>
<latestVersion>1.1</latestVersion>
<latestVersionCode>2</latestVersionCode>
<url>https://github.com/Shabinder/SpotiFlyer/releases</url>
</update>
</AppUpdater>

View File

@ -1,3 +1,20 @@
/*
* 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/>.
*/
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext{ ext{
@ -7,13 +24,14 @@ buildscript {
repositories { repositories {
google() google()
jcenter() jcenter()
mavenCentral()
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:4.0.1" classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//safe-Args //safe-Args
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
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
} }