diff --git a/.idea/dictionaries/shabinder.xml b/.idea/dictionaries/shabinder.xml index 16f2713a..599ffd82 100644 --- a/.idea/dictionaries/shabinder.xml +++ b/.idea/dictionaries/shabinder.xml @@ -1,7 +1,10 @@ + ffmpeg flyer + insta + instagram moshi musicforeveryone musicplaceholder diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 4dcbce12..bd3984c7 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -31,5 +31,10 @@ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 8940d6bc..d71765eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs.kotlin" - +apply plugin: 'kotlinx-serialization' android { compileSdkVersion 29 @@ -28,16 +28,14 @@ android { buildFeatures{ dataBinding = true - viewBinding = true } defaultConfig { applicationId 'com.shabinder.spotiflyer' minSdkVersion 22 targetSdkVersion 29 - versionCode 1 - versionName "1.0" - + versionCode 2 + versionName "1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -51,15 +49,20 @@ android { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } + lintOptions { + abortOnError false + } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } + + dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) 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.browser:browser:1.2.0' 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-android:1.3.7" -// implementation "androidx.room:room-runtime:2.2.5" -// kapt "androidx.room:room-compiler:2.2.5" -// implementation "androidx.room:room-ktx:2.2.5" - implementation "com.github.bumptech.glide:glide:4.11.0" - kapt "com.github.bumptech.glide:compiler:4.11.0" + implementation "androidx.room:room-runtime:2.2.5" + kapt "androidx.room:room-compiler:2.2.5" + implementation "androidx.room:room-ktx:2.2.5" + implementation ("com.github.bumptech.glide:recyclerview-integration: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 '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.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.retrofit2:retrofit:2.9.0' @@ -90,8 +97,16 @@ dependencies { implementation "com.squareup.moshi:moshi-kotlin:1.9.3" 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.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' testImplementation 'junit:junit:4.13' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 011f3294..37f088d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,9 +26,12 @@ - + + + - - - +--> + + \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/App.kt b/app/src/main/java/com/shabinder/spotiflyer/App.kt new file mode 100644 index 00000000..82a589b0 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/App.kt @@ -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 . + */ + +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 . + */ + +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) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 9c675e07..5d1434cf 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -18,23 +18,26 @@ package com.shabinder.spotiflyer import android.Manifest -import android.app.DownloadManager import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.net.ConnectivityManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil 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.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.downloadHelper.DownloadHelper import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.SpotifyServiceToken import com.shabinder.spotiflyer.utils.YoutubeInterface +import com.shabinder.spotiflyer.utils.createDirectory import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -48,12 +51,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory @Suppress("DEPRECATION") -class MainActivity : AppCompatActivity() ,DownloadHelper{ +class MainActivity : AppCompatActivity(){ private lateinit var binding: MainActivityBinding private var ytDownloader : YoutubeDownloader? = null private var spotifyService : SpotifyService? = null private var spotifyServiceToken : SpotifyServiceToken? = null - private var downloadManager : DownloadManager? = null // private val redirectUri = "spotiflyer://callback" private val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d" private val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8" @@ -63,20 +65,21 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ private var token :String ="" private lateinit var sharedViewModel: SharedViewModel + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this,R.layout.main_activity) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedPref = this.getPreferences(Context.MODE_PRIVATE) -// if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ -// val savedToken = sharedPref?.getString("token","error")!! -// sharedViewModel.accessToken.value = savedToken -// Log.i("SharedPrefs Token:",savedToken) -// token = savedToken -// -// implementSpotifyService(savedToken) -// }else{authenticateSpotify()} +/* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ + val savedToken = sharedPref?.getString("token","error")!! + sharedViewModel.accessToken.value = savedToken + Log.i("SharedPrefs Token:",savedToken) + token = savedToken + + implementSpotifyService(savedToken) + }else{authenticateSpotify()}*/ if(sharedViewModel.spotifyService == null){ authenticateSpotify() @@ -85,6 +88,12 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ } 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"} ytDownloader = YoutubeDownloader() @@ -92,32 +101,9 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{ //Initialing Communication with Youtube 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() } - /** * 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() { val builder = AuthenticationRequest.Builder(clientId,AuthenticationResponse.Type.TOKEN,redirectUri) diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt index 350f72ca..86a3d69d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt @@ -17,9 +17,9 @@ package com.shabinder.spotiflyer -import android.app.DownloadManager import android.content.Context import android.content.res.Resources +import android.os.Environment import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.github.kiulian.downloader.YoutubeDownloader @@ -32,15 +32,16 @@ import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import java.io.File class SharedViewModel : ViewModel() { var intentString = "" var accessToken = MutableLiveData().apply { value = "" } var spotifyService : SpotifyService? = null var ytDownloader : YoutubeDownloader? = null - var downloadManager : DownloadManager? = null var isConnected = MutableLiveData().apply { value = false } var easyUpiPayment: EasyUpiPayment? = null + val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator private var viewModelJob = Job() @@ -64,13 +65,12 @@ class SharedViewModel : ViewModel() { } fun showAlertDialog(resources:Resources,context: Context){ - val dialog = MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme) + 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 } - .setBackground(resources.getDrawable(R.drawable.gradient)) .show() } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt index 01e8fa0e..762e8fbf 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt @@ -17,22 +17,28 @@ package com.shabinder.spotiflyer.downloadHelper -import android.app.DownloadManager -import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED -import android.net.Uri +import android.content.Context +import android.content.Intent import android.os.Environment import android.util.Log +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.fragments.MainFragment +import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.utils.YoutubeInterface +import com.shabinder.spotiflyer.worker.ForegroundService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext 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() /** * Function To Download All Tracks Available in a List @@ -40,111 +46,106 @@ interface DownloadHelper { suspend fun downloadAllTracks( type:String, subFolder: String?, - trackList: List, ytDownloader: YoutubeDownloader?, downloadManager: DownloadManager?) { - trackList.forEach { downloadTrack(null,type,subFolder,ytDownloader,downloadManager,"${it.name} ${it.artists?.get(0)?.name ?:""}") } + trackList: List, ytDownloader: YoutubeDownloader?) { + 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( mainFragment: MainFragment?, type:String, subFolder:String?, ytDownloader: YoutubeDownloader?, - downloadManager: DownloadManager?, - searchQuery: String + searchQuery: String, + track: Track, + index: Int? = null ) { withContext(Dispatchers.IO) { - val data = YoutubeInterface.search(searchQuery)?.get(0) - if (data == null) { - Log.i("DownloadHelper", "Youtube Request Failed!") - } else { + val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!! - val video = ytDownloader?.getVideo(data.id) - //Fetching a Video Object. - val details = video?.details() - try{ - val format: Format = - video?.findAudioWithQuality(AudioQuality.medium)?.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()) + //Fetching a Video Object. + try { + val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") + } + downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment) + } catch (e: java.lang.IndexOutOfBoundsException) { + try { + val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") } - }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){ - 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) { + try { + val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data) + withContext(Dispatchers.Main) { + mainFragment?.showToast("Starting Download") } + downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment) + } catch (e: java.lang.IndexOutOfBoundsException) { + Log.i("Catch", e.toString()) } } - - } + } } - /** - * Downloading Using Android Download Manager - * */ - suspend fun downloadFile(url: String, downloadManager: DownloadManager?, title: String,subFolder: String?,type: String) { + private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String { + val video = ytDownloader?.getVideo(data.id) + val format: Format = + 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) { - val audioUri = Uri.parse(url) - val outputDir:String = - File.separator + "SpotiFlyer" + File.separator + type + File.separator + (if(subFolder == null){""}else{subFolder + File.separator}) + "${removeIllegalChars(title)}.mp3" + val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + + DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a") - val request = DownloadManager.Request(audioUri) - .setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE + if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){ + val downloadObject = DownloadObject( + track = track, + url = url, + outputDir = outputFile ) - .setAllowedOverRoaming(false) - .setTitle(title) - .setDescription("Spotify Downloader Working Up here...") - .setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir) - .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - downloadManager?.enqueue(request) - Log.i("DownloadManager", "Download Request Sent") - + Log.i("DH",outputFile) + if(index==null){ + downloadList.add(downloadObject) + }else{ + downloadList.add(downloadObject) + startService(context!!, downloadList) + downloadList = arrayListOf() + } + }else{withContext(Dispatchers.Main){mainFragment?.showToast("${track.name} is already Downloaded")}} } } + private fun startService(context:Context,list: ArrayList) { + val serviceIntent = Intent(context, ForegroundService::class.java) + serviceIntent.putParcelableArrayListExtra("list",list) + ContextCompat.startForegroundService(context, serviceIntent) + } + /** * Removing Illegal Chars from File Name * **/ - fun removeIllegalChars(fileName: String): String? { + private fun removeIllegalChars(fileName: String): String? { val illegalCharArray = charArrayOf( '/', '\n', @@ -161,12 +162,14 @@ interface DownloadHelper { '|', '\"', '.', - ':' + ':', + '-' ) var name = fileName for (c in illegalCharArray) { name = fileName.replace(c, '_') } + name = name.replace("\\s".toRegex(), "_") return name } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt index 27ead4a7..88f6fba3 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt @@ -23,31 +23,42 @@ import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.Uri import android.os.Bundle +import android.os.Environment import android.text.SpannableStringBuilder import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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 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.R import com.shabinder.spotiflyer.SharedViewModel import com.shabinder.spotiflyer.databinding.MainFragmentBinding import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.utils.SpotifyService import com.shabinder.spotiflyer.utils.bindImage +import com.shabinder.spotiflyer.utils.copyTo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.io.IOException @Suppress("DEPRECATION") -class MainFragment : Fragment(),DownloadHelper { +class MainFragment : Fragment() { private lateinit var binding:MainFragmentBinding private lateinit var mainViewModel: MainViewModel private lateinit var sharedViewModel: SharedViewModel @@ -56,12 +67,13 @@ class MainFragment : Fragment(),DownloadHelper { private var type:String = "" private var spotifyLink = "" private var i: Intent? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false) - + DownloadHelper.context = requireContext() sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) spotifyService = sharedViewModel.spotifyService @@ -73,13 +85,14 @@ class MainFragment : Fragment(),DownloadHelper { binding.usage.text = spanStringBuilder openSpotifyButton() + openGithubButton() + openInstaButton() binding.btnDonate.setOnClickListener { sharedViewModel.easyUpiPayment?.startPayment() } binding.btnSearch.setOnClickListener { - sharedViewModel.isConnected.value = isOnline() spotifyLink = binding.linkSearch.text.toString() val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') @@ -87,16 +100,15 @@ class MainFragment : Fragment(),DownloadHelper { Log.i("Fragment", "$type : $link") - if(sharedViewModel.spotifyService == null){ + if(sharedViewModel.spotifyService == null && !isOnline()){ (activity as MainActivity).authenticateSpotify() } if (type == "Error" || link == "Error") { showToast("Please Check Your Link!") - } else if(sharedViewModel.isConnected.value == false){ + } else if(!isOnline()){ sharedViewModel.showAlertDialog(resources,requireContext()) - } - else { + } else { adapter = TrackListAdapter() binding.trackList.adapter = adapter adapter.sharedViewModel = sharedViewModel @@ -106,7 +118,9 @@ class MainFragment : Fragment(),DownloadHelper { if(mainViewModel.searchLink == spotifyLink){ //it's a Device Configuration Change adapterConfig(mainViewModel.trackList) - bindImage(binding.imageView,mainViewModel.coverUrl) + sharedViewModel.uiScope.launch { + bindImage(binding.imageView,mainViewModel.coverUrl) + } }else{ when (type) { "track" -> { @@ -121,15 +135,14 @@ class MainFragment : Fragment(),DownloadHelper { adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { downloadAllTracks( "Tracks", null, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -142,22 +155,21 @@ class MainFragment : Fragment(),DownloadHelper { sharedViewModel.uiScope.launch { val albumObject = sharedViewModel.getAlbumDetails(link) val trackList = mutableListOf() - albumObject!!.tracks?.items?.forEach { trackList.add(it!!) } + albumObject!!.tracks?.items?.forEach { trackList.add(it) } mainViewModel.trackList = trackList mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!! bindImage(binding.imageView,mainViewModel.coverUrl) adapter.isAlbum = true adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { downloadAllTracks( "Albums", albumObject.name, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -171,21 +183,21 @@ class MainFragment : Fragment(),DownloadHelper { sharedViewModel.uiScope.launch { val playlistObject = sharedViewModel.getPlaylistDetails(link) val trackList = mutableListOf() - playlistObject!!.tracks?.items!!.forEach { trackList.add(it?.track!!) } + playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) } mainViewModel.trackList = trackList mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!! bindImage(binding.imageView,mainViewModel.coverUrl) adapterConfig(trackList) binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") sharedViewModel.uiScope.launch { withContext(Dispatchers.IO) { + loadAllImages(trackList) downloadAllTracks( "Playlists", playlistObject.name, trackList, - sharedViewModel.ytDownloader, - sharedViewModel.downloadManager - ) + sharedViewModel.ytDownloader) } } } @@ -213,6 +225,59 @@ class MainFragment : Fragment(),DownloadHelper { return binding.root } + /** + * Function to fetch all Images for using in mp3 tag. + **/ + private fun loadAllImages(trackList: List) { + 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 { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + 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() { val manager: PackageManager = requireActivity().packageManager 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 **/ private fun adapterConfig(trackList: List){ adapter.trackList = trackList.toList() adapter.totalItems = trackList.size + adapter.mainFragment = this adapter.notifyDataSetChanged() - } /** @@ -274,6 +357,10 @@ class MainFragment : Fragment(),DownloadHelper { 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 diff --git a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt index 4a274e1f..53a91ec4 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainViewModel.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.ViewModel import com.shabinder.spotiflyer.models.Track class MainViewModel: ViewModel() { - var searchLink:String = "" + var searchLink: String = "" var trackList = mutableListOf() - var coverUrl:String = "" + var coverUrl: String = "" } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt index afb70c8e..025a1e71 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Album( var album_type: String? = null, var artists: List? = null, @@ -33,6 +37,6 @@ data class Album( var popularity: Int? = null, var release_date: String? = null, var release_date_precision: String? = null, - var tracks: PagingObject? = null, + var tracks: PagingObjectTrack? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt index ba70b8a5..59a9ac54 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt @@ -16,10 +16,15 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Artist( var external_urls: Map? = null, var href: String? = null, var id: String? = null, var name: String? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt index b10f83a5..d16ba35d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt @@ -16,6 +16,11 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Copyright( var text: String? = null, - var type: String? = null) \ No newline at end of file + var type: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt new file mode 100644 index 00000000..adcef155 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt @@ -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 . + */ + +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 \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt index 4cf5e834..804d438d 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Episodes( var audio_preview_url:String?, var description:String?, @@ -35,4 +39,4 @@ data class Episodes( var release_date_precision:String?, var type:String?, var uri:String -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt index 45cae186..013dd195 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt @@ -17,6 +17,9 @@ package com.shabinder.spotiflyer.models +import kotlinx.serialization.Serializable + +@Serializable data class Followers( var href: String? = null, - var total: Int? = null) \ No newline at end of file + var total: Int? = null):java.io.Serializable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt index 61f67f17..764f59ad 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt @@ -16,7 +16,12 @@ */ package com.shabinder.spotiflyer.models + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Image( var width: Int? = null, var height: Int? = null, - var url: String? = null) \ No newline at end of file + var url: String? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt index 3efa8bb8..361378d4 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt @@ -17,9 +17,13 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class LinkedTrack( var external_urls: Map? = null, var href: String? = null, var id: String? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt new file mode 100644 index 00000000..3f298934 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt @@ -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 . + */ + +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? = null, + var limit: Int = 0, + var next: String? = null, + var offset: Int = 0, + var previous: String? = null, + var total: Int = 0): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt similarity index 82% rename from app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt index 603b4319..004a79ec 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObject.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt @@ -17,11 +17,15 @@ package com.shabinder.spotiflyer.models -data class PagingObject( +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class PagingObjectTrack( var href: String? = null, - var items: List? = null, + var items: List? = null, var limit: Int = 0, var next: String? = null, var offset: Int = 0, var previous: String? = null, - var total: Int = 0) \ No newline at end of file + var total: Int = 0):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt index 1cc01e6b..4842ae91 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt @@ -17,7 +17,11 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable import com.squareup.moshi.Json +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Playlist( @Json(name = "collaborative")var is_collaborative: Boolean? = null, var description: String? = null, @@ -30,6 +34,6 @@ data class Playlist( var owner: UserPublic? = null, @Json(name = "public")var is_public: Boolean? = null, var snapshot_id: String? = null, - var tracks: PagingObject? = null, + var tracks: PagingObjectPlaylistTrack? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt index 0eba9d32..56a5d103 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt @@ -17,8 +17,12 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class PlaylistTrack( var added_at: String? = null, var added_by: UserPublic? = null, var track: Track? = null, - var is_local: Boolean? = null) \ No newline at end of file + var is_local: Boolean? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt index 0b69ef32..c2fdb85b 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt @@ -17,8 +17,12 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Token( var access_token:String, var token_type:String, var expires_in:Int -) \ No newline at end of file +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt index b1ef82d8..e7e1427a 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class Track( var artists: List? = null, var available_markets: List? = null, @@ -35,4 +39,4 @@ data class Track( var uri: String? = null, var album: Album? = null, var external_ids: Map? = null, - var popularity: Int? = null) \ No newline at end of file + var popularity: Int? = null):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt b/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt index ed1db284..178798dd 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class UserPrivate( val country:String, var display_name: String, @@ -28,4 +32,4 @@ data class UserPrivate( var images: List? = null, var product:String, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt b/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt index 9a6b636b..06df67cd 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt @@ -17,6 +17,10 @@ package com.shabinder.spotiflyer.models +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize data class UserPublic( var display_name: String? = null, var external_urls: Map? = null, @@ -25,4 +29,4 @@ data class UserPublic( var id: String? = null, var images: List? = null, var type: String? = null, - var uri: String? = null) \ No newline at end of file + var uri: String? = null): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt index 033435e3..e6be791e 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt @@ -26,13 +26,14 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.shabinder.spotiflyer.R 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.models.Track import com.shabinder.spotiflyer.utils.bindImage import kotlinx.coroutines.launch -class TrackListAdapter:RecyclerView.Adapter(),DownloadHelper { + +class TrackListAdapter:RecyclerView.Adapter() { var trackList = listOf() var totalItems:Int = 0 @@ -52,7 +53,9 @@ class TrackListAdapter:RecyclerView.Adapter(),Downl override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = trackList[position] 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}}" @@ -60,7 +63,7 @@ class TrackListAdapter:RecyclerView.Adapter(),Downl holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec" holder.downloadBtn.setOnClickListener{ 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) } } diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt index 5052ad65..876bfd18 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt @@ -17,22 +17,102 @@ 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 androidx.core.net.toUri import androidx.databinding.BindingAdapter 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.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") fun bindImage(imgView: ImageView, imgUrl: String?) { imgUrl?.let { val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() - Glide.with(imgView.context) + Glide + .with(imgView.context) + .asFile() .load(imgUri) - .apply(RequestOptions() - .placeholder(R.drawable.ic_song_placeholder) - .error(R.drawable.ic_musicplaceholder)) - .into(imgView) + .placeholder(R.drawable.ic_song_placeholder) + .error(R.drawable.ic_musicplaceholder) + .listener(object:RequestListener{ + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + 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() + } } -} \ No newline at end of file + +/** + *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")} +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt new file mode 100644 index 00000000..c97f3a71 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt @@ -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 . + */ + +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() + private var serviceJob = Job() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + private val requestMap = mutableMapOf() + private val downloadMap = mutableMapOf() + 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 + val list = intent.getParcelableArrayListExtra("list") ?: intent.extras?.getParcelableArrayList("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, + 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() + 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() + 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() + 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 + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/down_arrowbw.png b/app/src/main/res/drawable/down_arrowbw.png new file mode 100644 index 00000000..b7346231 Binary files /dev/null and b/app/src/main/res/drawable/down_arrowbw.png differ diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 00000000..050eff17 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..c94771eb --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_instagram.xml b/app/src/main/res/drawable/ic_instagram.xml new file mode 100644 index 00000000..b99f87ce --- /dev/null +++ b/app/src/main/res/drawable/ic_instagram.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mug.xml b/app/src/main/res/drawable/ic_mug.xml index 52f1237d..7fe934f1 100644 --- a/app/src/main/res/drawable/ic_mug.xml +++ b/app/src/main/res/drawable/ic_mug.xml @@ -1,5 +1,22 @@ - + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index 386a17e0..220b838d 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -134,7 +134,7 @@ + + + + + + + + + + + + - - + android:id="@+id/track_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="26dp" + android:visibility="gone" + 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/open_spotify" /> diff --git a/app/src/main/res/layout/track_list_item.xml b/app/src/main/res/layout/track_list_item.xml index a1197185..5a2e1c88 100644 --- a/app/src/main/res/layout/track_list_item.xml +++ b/app/src/main/res/layout/track_list_item.xml @@ -1,5 +1,22 @@ + + @@ -15,7 +32,6 @@ android:layout_width="100dp" android:layout_height="80dp" android:contentDescription="Track Image" - android:visibility="visible" android:scaleType="centerInside" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/artist" diff --git a/app/src/main/res/xml/app_update.xml b/app/src/main/res/xml/app_update.xml new file mode 100644 index 00000000..28cc79a2 --- /dev/null +++ b/app/src/main/res/xml/app_update.xml @@ -0,0 +1,25 @@ + + + + + + 1.1 + 2 + https://github.com/Shabinder/SpotiFlyer/releases + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 31b62629..3243adf8 100644 --- a/build.gradle +++ b/build.gradle @@ -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 . + */ + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext{ @@ -7,13 +24,14 @@ buildscript { repositories { google() jcenter() - + mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //safe-Args 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 // in the individual module build.gradle files }