diff --git a/.idea/dictionaries/shabinder.xml b/.idea/dictionaries/shabinder.xml index 599ffd82..b5d414c9 100644 --- a/.idea/dictionaries/shabinder.xml +++ b/.idea/dictionaries/shabinder.xml @@ -5,6 +5,7 @@ flyer insta instagram + maxresdefault moshi musicforeveryone musicplaceholder @@ -15,6 +16,8 @@ spotify spotifydownloader spotifyler + thru + youtu \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 222d7c31..d3c66e7a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,8 @@ + + + diff --git a/app/build.gradle b/app/build.gradle index d71765eb..22d54822 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' +//apply plugin: 'kotlinx-serialization' android { compileSdkVersion 29 @@ -34,17 +34,28 @@ android { applicationId 'com.shabinder.spotiflyer' minSdkVersion 22 targetSdkVersion 29 - versionCode 2 - versionName "1.1" + versionCode 3 + versionName "1.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - + packagingOptions { + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/LICENSE' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/license.txt' + exclude 'META-INF/NOTICE' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/notice.txt' + exclude 'META-INF/ASL2.0' + exclude("META-INF/*.kotlin_module") + } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 @@ -65,6 +76,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.browser:browser:1.2.0' + implementation 'androidx.webkit:webkit:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' @@ -86,9 +98,14 @@ dependencies { } 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' +// Authentication Way Changed! +// implementation ('com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'){ +// exclude module: 'httpclient' +// } +// //noinspection GradleDependency +// implementation ('com.google.oauth-client:google-oauth-client:1.22.0'){ +// exclude module: 'httpclient' +// } // implementation 'com.spotify.android:auth:1.1.0' implementation 'com.squareup.okhttp3:okhttp:4.8.0' @@ -97,9 +114,6 @@ 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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37f088d9..0d4c26b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,8 @@ + + diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 5d1434cf..788dc826 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -18,6 +18,7 @@ package com.shabinder.spotiflyer import android.Manifest +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -25,6 +26,8 @@ import android.net.ConnectivityManager import android.net.Uri import android.os.Build import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil @@ -36,7 +39,6 @@ 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 @@ -71,6 +73,8 @@ class MainActivity : AppCompatActivity(){ binding = DataBindingUtil.setContentView(this,R.layout.main_activity) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedPref = this.getPreferences(Context.MODE_PRIVATE) + //starting Notification and Downloader Service! + DownloadHelper.startService(this) /* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){ val savedToken = sharedPref?.getString("token","error")!! @@ -88,6 +92,7 @@ class MainActivity : AppCompatActivity(){ } requestPermission() + disableDozeMode() checkIfLatestVersion() createDir() setUpi() @@ -98,12 +103,42 @@ class MainActivity : AppCompatActivity(){ //Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"} ytDownloader = YoutubeDownloader() sharedViewModel.ytDownloader = ytDownloader - //Initialing Communication with Youtube - YoutubeInterface.youtubeConnector() handleIntentFromExternalActivity() } + @SuppressLint("BatteryLife") + fun disableDozeMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = + this.getSystemService(Context.POWER_SERVICE) as PowerManager + val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName) + if (!isIgnoringBatteryOptimizations) { + val intent = Intent() + intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + intent.data = Uri.parse("package:$packageName") + startActivityForResult(intent, 1233) + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == 1233) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = + getSystemService(Context.POWER_SERVICE) as PowerManager + val isIgnoringBatteryOptimizations = + pm.isIgnoringBatteryOptimizations(packageName) + if (isIgnoringBatteryOptimizations) { + // Ignoring battery optimization + } else { + disableDozeMode()//Again Ask For Permission!! + } + } + } + } + /** * Adding my own new Spotify Web Api Requests! * */ @@ -253,6 +288,7 @@ class MainActivity : AppCompatActivity(){ createDirectory(DownloadHelper.defaultDir+"Tracks/") createDirectory(DownloadHelper.defaultDir+"Albums/") createDirectory(DownloadHelper.defaultDir+"Playlists/") + createDirectory(DownloadHelper.defaultDir+"YT_Downloads/") } private fun checkIfLatestVersion() { 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 022cf581..6cc76497 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt @@ -17,28 +17,44 @@ package com.shabinder.spotiflyer.downloadHelper +import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.os.Build import android.os.Environment import android.util.Log +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.webkit.ValueCallback +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.TextView import androidx.core.content.ContextCompat import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.model.formats.Format import com.github.kiulian.downloader.model.quality.AudioQuality +import com.shabinder.spotiflyer.SharedViewModel 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.launch import kotlinx.coroutines.withContext import java.io.File object DownloadHelper { - + var webView:WebView? = null var context : Context? = null + var statusBar:TextView? = null val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator private var downloadList = arrayListOf() + var sharedViewModel:SharedViewModel? = null + private var isBrowserLoading = false + private var total = 0 + private var Processed = 0 + var youtubeList = mutableListOf() /** * Function To Download All Tracks Available in a List @@ -47,108 +63,179 @@ object DownloadHelper { type:String, subFolder: String?, 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? = null, - type:String, - subFolder:String?, - ytDownloader: YoutubeDownloader?, - searchQuery: String, - track: Track, - index: Int? = null - ) { - withContext(Dispatchers.IO) { - val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!! - - //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") - } - 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") + withContext(Dispatchers.Main){ + var size = trackList.size + total += size + animateStatusBar() + trackList.forEach { + size-- + val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + + defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(it.name!!)+".mp3") + if(File(outputFile).exists()){//Download Already Present!! + Processed++ + updateStatusBar() + }else{ + if(isBrowserLoading){ + if(size == 0){ + youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )) + }else{ + youtubeList.add(YoutubeRequest(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)) + } + }else{ + if(size == 0){ + getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 ) + }else{ + getYTLink(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it) } - 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 { - 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 + + //TODO CleanUp here and there!! + @SuppressLint("SetJavaScriptEnabled") + suspend fun getYTLink(mainFragment: MainFragment? = null, + type:String, + subFolder:String?, + ytDownloader: YoutubeDownloader?, + searchQuery: String, + track: Track, + index: Int? = null){ + val searchText = searchQuery.replace("\\s".toRegex(), "+") + val url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=$searchText" + Log.i("DH YT LINK ",url) + applyWebViewSettings(webView!!) + withContext(Dispatchers.Main){ + isBrowserLoading = true + webView!!.loadUrl(url) + webView!!.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.evaluateJavascript( + "document.getElementsByClassName(\"yt-simple-endpoint style-scope ytd-video-renderer\")[0].href" + ,object :ValueCallback{ + override fun onReceiveValue(value: String?) { + Log.i("YT-id",value.toString().replace("\"","")) + val id = value!!.substringAfterLast("=", "error").replace("\"","") + Log.i("YT-id",id) + if(id !="error"){//Link extracting error + mainFragment?.showToast("Starting Download") + Processed++ + updateStatusBar() + downloadFile(subFolder, type, track, index,ytDownloader,id) + } + if(youtubeList.isNotEmpty()){ + val request = youtubeList[0] + sharedViewModel!!.uiScope.launch { + getYTLink(request.mainFragment,request.type,request.subFolder,request.ytDownloader,request.searchQuery,request.track,request.index) + } + youtubeList.remove(request) + if(youtubeList.size == 0){//list processing completed , webView is free again! + isBrowserLoading = false + } + } + } + } ) + } + } + } + + } + + @SuppressLint("SetJavaScriptEnabled") + fun applyWebViewSettings(webView: WebView) { + val desktopUserAgent = + "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.4) Gecko/20100101 Firefox/4.0" + val mobileUserAgent = + "Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30" + + //Choose Mobile/Desktop client. + webView.settings.userAgentString = desktopUserAgent + webView.settings.loadWithOverviewMode = true + webView.settings.loadWithOverviewMode = true + webView.settings.builtInZoomControls = true + webView.settings.setSupportZoom(true) + webView.isScrollbarFadingEnabled = false + webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY + webView.settings.displayZoomControls = false + webView.settings.useWideViewPort = true + webView.settings.javaScriptEnabled = true + webView.settings.loadsImagesAutomatically = false + webView.settings.blockNetworkImage = true + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + webView.settings.safeBrowsingEnabled = true + } + } + + private fun updateStatusBar() { + statusBar!!.visibility = View.VISIBLE + statusBar?.text = "Total: $total Processed: $Processed" } - private suspend fun downloadFile(url: String, title: String, subFolder: String?, type: String, track:Track, index:Int? = null,mainFragment: MainFragment? = null) { - withContext(Dispatchers.IO) { - 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") + fun downloadFile(subFolder: String?, type: String, track:Track, index:Int? = null,ytDownloader: YoutubeDownloader?,id: String) { + sharedViewModel!!.uiScope.launch { + withContext(Dispatchers.IO) { + val video = ytDownloader?.getVideo(id) + val format:Format? =try { + video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format + }catch (e:java.lang.IndexOutOfBoundsException){ + try { + video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format + }catch (e:java.lang.IndexOutOfBoundsException){ + try{ + video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format + }catch (e:java.lang.IndexOutOfBoundsException){ + Log.i("YTDownloader",e.toString()) + null + } + } + } + format?.let { + val url:String = format.url() - if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){ - val downloadObject = DownloadObject( - track = track, - url = url, - outputDir = outputFile - ) - Log.i("DH",outputFile) - if(index==null){ + Log.i("DHelper Link Found", url) + + val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + + defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a") + + val downloadObject = DownloadObject( + track = track, + url = url, + outputDir = outputFile + ) + Log.i("DH",outputFile) + startService(context!!, downloadObject) + + /*if(index==null){ downloadList.add(downloadObject) }else{ downloadList.add(downloadObject) startService(context!!, downloadList) + Log.i("DH No of Songs", downloadList.size.toString()) downloadList = arrayListOf() - } - }else{withContext(Dispatchers.Main){ - mainFragment?.showToast("${track.name} is already Downloaded") + }*/ +// downloadList.add(downloadObject) +// downloadList = arrayListOf() } } } } - private fun startService(context:Context,list: ArrayList) { + fun startService(context:Context,obj:DownloadObject? = null ) { val serviceIntent = Intent(context, ForegroundService::class.java) - serviceIntent.putParcelableArrayListExtra("list",list) + serviceIntent.putExtra("object",obj) ContextCompat.startForegroundService(context, serviceIntent) } /** * Removing Illegal Chars from File Name * **/ - private fun removeIllegalChars(fileName: String): String? { + fun removeIllegalChars(fileName: String): String? { val illegalCharArray = charArrayOf( '/', '\n', @@ -165,7 +252,6 @@ object DownloadHelper { '|', '\"', '.', - ':', '-', '\'' ) @@ -180,6 +266,29 @@ object DownloadHelper { name = name.replace("\\[".toRegex(), "") name = name.replace("]".toRegex(), "") name = name.replace("\\.".toRegex(), "") + name = name.replace("\"".toRegex(), "") + name = name.replace("\'".toRegex(), "") + name = name.replace(":".toRegex(), "") + name = name.replace("\\|".toRegex(), "") return name } -} \ No newline at end of file + + private fun animateStatusBar() { + val anim: Animation = AlphaAnimation(0.0f, 0.9f) + anim.duration = 650 //You can manage the blinking time with this parameter + anim.startOffset = 20 + anim.repeatMode = Animation.REVERSE + anim.repeatCount = Animation.INFINITE + statusBar?.animation = anim + } + +} +data class YoutubeRequest( + val mainFragment: MainFragment? = null, + val type:String, + val subFolder:String?, + val ytDownloader: YoutubeDownloader?, + val searchQuery: String, + val track: Track, + val index: Int? = null +) \ 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 88f6fba3..044ee832 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/fragments/MainFragment.kt @@ -17,6 +17,7 @@ package com.shabinder.spotiflyer.fragments +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.pm.PackageManager @@ -29,6 +30,9 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.webkit.ValueCallback +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.Toast import androidx.core.net.toUri import androidx.databinding.DataBindingUtil @@ -45,6 +49,7 @@ 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.applyWebViewSettings import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.recyclerView.TrackListAdapter @@ -67,23 +72,25 @@ class MainFragment : Fragment() { private var type:String = "" private var spotifyLink = "" private var i: Intent? = null + private var webView: WebView? = null + + @SuppressLint("SetJavaScriptEnabled") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false) + webView = binding.webView + DownloadHelper.webView = binding.webView DownloadHelper.context = requireContext() sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) spotifyService = sharedViewModel.spotifyService + DownloadHelper.sharedViewModel = sharedViewModel + DownloadHelper.statusBar = binding.StatusBar - val spanStringBuilder = SpannableStringBuilder() - spanStringBuilder.append(getText(R.string.d_one)).append("\n") - spanStringBuilder.append(getText(R.string.d_two)).append("\n") - spanStringBuilder.append(getText(R.string.d_three)).append("\n") - - binding.usage.text = spanStringBuilder + setUpUsageText() openSpotifyButton() openGithubButton() openInstaButton() @@ -93,127 +100,14 @@ class MainFragment : Fragment() { } binding.btnSearch.setOnClickListener { - spotifyLink = binding.linkSearch.text.toString() - - val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') - type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') - - Log.i("Fragment", "$type : $link") - - if(sharedViewModel.spotifyService == null && !isOnline()){ - (activity as MainActivity).authenticateSpotify() + val link = binding.linkSearch.text.toString() + if(link.contains("open.spotify",true)){ + spotifySearch() + } + if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){ + youtubeSearch() } - if (type == "Error" || link == "Error") { - showToast("Please Check Your Link!") - } else if(!isOnline()){ - sharedViewModel.showAlertDialog(resources,requireContext()) - } else { - adapter = TrackListAdapter() - binding.trackList.adapter = adapter - adapter.sharedViewModel = sharedViewModel - adapter.mainFragment = this - setUiVisibility() - - if(mainViewModel.searchLink == spotifyLink){ - //it's a Device Configuration Change - adapterConfig(mainViewModel.trackList) - sharedViewModel.uiScope.launch { - bindImage(binding.imageView,mainViewModel.coverUrl) - } - }else{ - when (type) { - "track" -> { - mainViewModel.searchLink = spotifyLink - sharedViewModel.uiScope.launch { - val trackObject = sharedViewModel.getTrackDetails(link) - val trackList = mutableListOf() - trackList.add(trackObject!!) - mainViewModel.trackList = trackList - mainViewModel.coverUrl = trackObject.album!!.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) { - downloadAllTracks( - "Tracks", - null, - trackList, - sharedViewModel.ytDownloader) - } - } - } - - } - } - - "album" -> { - mainViewModel.searchLink = spotifyLink - sharedViewModel.uiScope.launch { - val albumObject = sharedViewModel.getAlbumDetails(link) - val trackList = mutableListOf() - 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) - } - } - } - } - - - } - - "playlist" -> { - mainViewModel.searchLink = spotifyLink - sharedViewModel.uiScope.launch { - val playlistObject = sharedViewModel.getPlaylistDetails(link) - val trackList = mutableListOf() - 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) - } - } - } - } - - } - - "episode" -> { - showToast("Implementation Pending") - } - "show" -> { - showToast("Implementation Pending ") - } - } - } - } } handleIntent() //Handling Device Configuration Change @@ -225,6 +119,173 @@ class MainFragment : Fragment() { return binding.root } + private fun youtubeSearch() { + val youtubeLink = binding.linkSearch.text.toString() + var title = "" + val link = youtubeLink.removePrefix("https://").removePrefix("http://") + val sampleDomain1 = "youtube.com" + val sampleDomain2 = "youtu.be" + if(!link.contains("playlist",true)){ + var searchId = "error" + if(link.contains(sampleDomain1,true) ){ + searchId = link.substringAfterLast("=","error") + } + if(link.contains(sampleDomain2,true) && !link.contains("playlist",true) ){ + searchId = link.substringAfterLast("/","error") + } + if(searchId != "error"){ + val coverLink = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" + applyWebViewSettings(webView!!) + sharedViewModel.uiScope.launch { + webView!!.loadUrl(youtubeLink) + webView!!.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + view?.evaluateJavascript( + "document.getElementsByTagName(\"h1\")[0].textContent" + ,object : ValueCallback { + override fun onReceiveValue(value: String?) { + title = DownloadHelper.removeIllegalChars(value.toString()).toString() + Log.i("YT-id", title) + Log.i("YT-id", value) + Log.i("YT-id", coverLink) + setUiVisibility() + bindImage(binding.imageView,coverLink) + binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") + //TODO Clean This Code! + DownloadHelper.downloadFile(null,"YT_Downloads",Track(name = value,ytCoverUrl = coverLink),0,sharedViewModel.ytDownloader,searchId) + } + } + }) + } + } + } + } + }else(showToast("Your Youtube Link is not of a Video!!")) + } + + private fun spotifySearch(){ + spotifyLink = binding.linkSearch.text.toString() + + val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') + type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') + + Log.i("Fragment", "$type : $link") + + if(sharedViewModel.spotifyService == null && !isOnline()){ + (activity as MainActivity).authenticateSpotify() + } + + if (type == "Error" || link == "Error") { + showToast("Please Check Your Link!") + } else if(!isOnline()){ + sharedViewModel.showAlertDialog(resources,requireContext()) + } else { + adapter = TrackListAdapter() + binding.trackList.adapter = adapter + adapter.sharedViewModel = sharedViewModel + adapter.mainFragment = this + setUiVisibility() + + if(mainViewModel.searchLink == spotifyLink){ + //it's a Device Configuration Change + adapterConfig(mainViewModel.trackList) + sharedViewModel.uiScope.launch { + bindImage(binding.imageView,mainViewModel.coverUrl) + } + }else{ + when (type) { + "track" -> { + mainViewModel.searchLink = spotifyLink + sharedViewModel.uiScope.launch { + val trackObject = sharedViewModel.getTrackDetails(link) + val trackList = mutableListOf() + trackList.add(trackObject!!) + mainViewModel.trackList = trackList + mainViewModel.coverUrl = trackObject.album!!.images?.get(0)!!.url!! + bindImage(binding.imageView,mainViewModel.coverUrl) + adapterConfig(trackList) + + binding.btnDownloadAll.setOnClickListener { + showToast("Starting Download in Few Seconds") + sharedViewModel.uiScope.launch { + downloadAllTracks( + "Tracks", + null, + trackList, + sharedViewModel.ytDownloader + ) + } + } + + } + } + + "album" -> { + mainViewModel.searchLink = spotifyLink + sharedViewModel.uiScope.launch { + val albumObject = sharedViewModel.getAlbumDetails(link) + val trackList = mutableListOf() + 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 { + loadAllImages(trackList) + downloadAllTracks( + "Albums", + albumObject.name, + trackList, + sharedViewModel.ytDownloader + ) + } + } + } + + + } + + "playlist" -> { + mainViewModel.searchLink = spotifyLink + sharedViewModel.uiScope.launch { + val playlistObject = sharedViewModel.getPlaylistDetails(link) + val trackList = mutableListOf() + 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 { + loadAllImages(trackList) + downloadAllTracks( + "Playlists", + playlistObject.name, + trackList, + sharedViewModel.ytDownloader + ) + } + } + } + } + + "episode" -> { + showToast("Implementation Pending") + } + "show" -> { + showToast("Implementation Pending ") + } + } + } + } + } + /** * Function to fetch all Images for using in mp3 tag. **/ @@ -351,6 +412,15 @@ class MainFragment : Fragment() { }) } + private fun setUpUsageText() { + val spanStringBuilder = SpannableStringBuilder() + spanStringBuilder.append(getText(R.string.d_one)).append("\n") + spanStringBuilder.append(getText(R.string.d_two)).append("\n") + spanStringBuilder.append(getText(R.string.d_three)).append("\n") + binding.usage.text = spanStringBuilder + } + + /** * Util. Function to create toasts! **/ 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 013dd195..8a198e09 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt @@ -17,9 +17,10 @@ package com.shabinder.spotiflyer.models -import kotlinx.serialization.Serializable +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize -@Serializable +@Parcelize data class Followers( var href: String? = null, - var total: Int? = null):java.io.Serializable \ No newline at end of file + var total: Int? = null):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 e7e1427a..d16e764b 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt @@ -39,4 +39,5 @@ data class Track( var uri: String? = null, var album: Album? = null, var external_ids: Map? = null, - var popularity: Int? = null):Parcelable \ No newline at end of file + var popularity: Int? = null, + var ytCoverUrl: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 e6be791e..c687dcf3 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt @@ -26,7 +26,7 @@ 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.downloadTrack +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.getYTLink import com.shabinder.spotiflyer.fragments.MainFragment import com.shabinder.spotiflyer.models.Track import com.shabinder.spotiflyer.utils.bindImage @@ -63,7 +63,7 @@ class TrackListAdapter:RecyclerView.Adapter() { 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,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0) + getYTLink(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 876bfd18..d754bded 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt @@ -72,7 +72,7 @@ fun bindImage(imgView: ImageView, imgUrl: String?) { try { val file = File( Environment.getExternalStorageDirectory(), - DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg" + DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".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 diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/YoutubeInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/YoutubeInterface.kt deleted file mode 100644 index f86a47f0..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/YoutubeInterface.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2020 Shabinder Singh - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.utils - -import android.util.Log -import com.google.api.client.http.HttpRequestInitializer -import com.google.api.client.http.javanet.NetHttpTransport -import com.google.api.client.json.jackson2.JacksonFactory -import com.google.api.services.youtube.YouTube -import java.io.IOException - -object YoutubeInterface { - private var youtube: YouTube? = null - private var query:YouTube.Search.List? = null - private var apiKey:String = "AIzaSyDuRmMA_2mF56BjlhhNpa0SIbjMgjjFaEI" - private var apiKey2:String = "AIzaSyCotyqgqmz5qw4-IH0tiezIrIIDHLI2yNs" - - fun youtubeConnector() { - youtube = - YouTube.Builder(NetHttpTransport(), JacksonFactory(), HttpRequestInitializer { }) - .setApplicationName("spotifyler").build() - try { - query = youtube?.search()?.list("id,snippet") - query?.key = apiKey - query?.maxResults = 1 - query?.type = "video" - query?.fields = - "items(id/videoId,snippet/title,snippet/thumbnails/default/url)" - } catch (e: IOException) { - Log.i("YI", "Could not initialize: $e") - } - } - - fun search(keywords: String?): List? { - Log.i("YI searched for",keywords.toString()) - if (youtube == null){youtubeConnector()} - query!!.q= keywords - return try { - val response = query!!.execute() - val results = - response.items - val items = mutableListOf() - for (result in results) { - val item = VideoItem( - id = result.id.videoId, - title = result.snippet.title, -// description = result.snippet.description, - thumbnailUrl = result.snippet.thumbnails.default.url - ) - items.add(item) - Log.i("YI links received",item.id) - } - items - } catch (e: IOException) { - Log.d("YI", "Could not search: $e") - if(query?.key == apiKey2){return null} - query?.key = apiKey2 - search(keywords) - } - } - - data class VideoItem( - val id:String, - val title:String, -// val description: String, - val thumbnailUrl:String - ) - -} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt index 9672da16..31bb2034 100644 --- a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt @@ -18,11 +18,16 @@ package com.shabinder.spotiflyer.worker import android.app.* +import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.net.Uri import android.os.Build import android.os.Environment import android.os.IBinder +import android.os.PowerManager import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat @@ -41,22 +46,41 @@ import com.shabinder.spotiflyer.models.Track import com.tonyodev.fetch2.* import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.Func -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.io.File import java.io.FileInputStream class ForegroundService : Service(){ private val tag = "Foreground Service" private val channelId = "ForegroundDownloaderService" + private val notificationId = 101 private var total = 0 //Total Downloads Requested private var converted = 0//Total Files Converted + private var downloaded = 0//Total Files downloaded private var fetch:Fetch? = null + private var downloadManager : DownloadManager? = null private var downloadList = mutableListOf() private var serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private val requestMap = mutableMapOf() private val downloadMap = mutableMapOf() private var speed :Long = 0 + private var defaultDirectory = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + private val parentDirectory = File(Environment.getExternalStorageDirectory(), + defaultDirectory+File.separator + ) + private var wakeLock: PowerManager.WakeLock? = null + private var isServiceStarted = false + private var messageSnippet1 = "" + private var messageSnippet2 = "" + private var messageSnippet3 = "" + private var messageSnippet4 = "" + var notificationLine = 1 + + override fun onBind(intent: Intent): IBinder? { return null @@ -69,26 +93,22 @@ class ForegroundService : Service(){ 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() + downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val fetchConfiguration = FetchConfiguration.Builder(this) .setDownloadConcurrentLimit(4) .build() - Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration) - fetch = Fetch.getDefaultInstance() + fetch = Fetch.Impl.getInstance(fetchConfiguration) +// fetch?.enableLogging(true) fetch?.addListener(fetchListener) startForeground() } + /** + *Starting Service with Notification as Foreground! + **/ private fun startForeground() { val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -105,10 +125,15 @@ class ForegroundService : Service(){ .setSubText("Speed: $speed KB/s ") .setNotificationSilent() .setOnlyAlertOnce(true) - .setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ") + .setContentText("Total: $total Downloaded: $downloaded Completed:$converted ") .setSmallIcon(R.drawable.down_arrowbw) + .setStyle(NotificationCompat.InboxStyle() + .addLine(messageSnippet1) + .addLine(messageSnippet2) + .addLine(messageSnippet3) + .addLine(messageSnippet4)) .build() - startForeground(101, notification) + startForeground(notificationId, notification) } @RequiresApi(Build.VERSION_CODES.O) @@ -126,52 +151,107 @@ class ForegroundService : Service(){ 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) + //val list = intent.getSerializableExtra("list") as List +// val list = intent.getParcelableArrayListExtra("list") ?: intent.extras?.getParcelableArrayList("list") +// Log.i(tag,"Intent List Size: ${list!!.size}") + val obj = intent.getParcelableExtra("object") ?: intent.extras?.getParcelable("object") + obj?.let { + total ++ +// Log.i(tag,"Intent List Size: ${list!!.size}") + updateNotification() + serviceScope.launch { + val request= Request(obj.url, obj.outputDir) request.priority = Priority.NORMAL request.networkType = NetworkType.ALL - fetch?.enqueue(request, + fetch!!.enqueue(request, Func { - Log.i("DownloadManager", "Download Request Sent") - requestMap[it] = downloadObject.track - downloadList.remove(downloadObject) }, + requestMap[it] = obj.track + downloadList.remove(obj) + Log.i(tag, "Enqueuing Download") + }, Func { - Log.i("DownloadManager", "Download Request Error:${it.throwable.toString()}")} + Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")} ) - - } - } } - return START_NOT_STICKY + + //Wake locks and misc tasks from here : + return if (isServiceStarted){ + START_STICKY + } else{ + Log.i(tag,"Starting the foreground service task") + isServiceStarted = true + + wakeLock = + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { + acquire() + } + } + START_STICKY + } } override fun onDestroy() { super.onDestroy() if(downloadMap.isEmpty() && converted == total){ Log.i(tag,"Service destroyed.") + fetch?.close() + deleteFile(parentDirectory) + releaseWakeLock() stopForeground(true) } } + private fun releaseWakeLock() { + Log.i(tag,"Releasing Wake Lock") + try { + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + } catch (e: Exception) { + Log.i(tag,"Service stopped without being started: ${e.message}") + } + isServiceStarted = false + } + override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) if(downloadMap.isEmpty() && converted == total ){ - Log.i(tag,"Service destroyed.") + Log.i(tag,"Service Removed.") + fetch?.close() stopSelf() } } + /** + * Deleting All Residual Files except Mp3 Files + * */ + private fun deleteFile(dir:File) { + Log.i(tag,"Starting Deletions in ${dir.path} ") + val fList = dir.listFiles() + fList?.let { + for (file in fList) { + if (file.isDirectory) { + Log.i(tag,"Cleaning ${file.path} Directory") + deleteFile(file) + } else if(file.isFile) { + if(file.path.toString().substringAfterLast(".") != "mp3"){ +// Log.i(tag,"deleting ${file.path}") + file.delete() + } + } + } + } + } + + /** + * Fetch Listener/ Responsible for Fetch Behaviour + **/ private var fetchListener: FetchListener = object : FetchListener { override fun onQueued( download: Download, @@ -194,7 +274,32 @@ class ForegroundService : Service(){ totalBlocks: Int ) { val track = requestMap[download.request] + when(notificationLine){ + 1 -> { + messageSnippet1 = "Downloading ${track?.name}" + notificationLine = 2 + } + 2 -> { + messageSnippet2 = "Downloading ${track?.name}" + notificationLine = 3 + } + 3-> { + messageSnippet3 = "Downloading ${track?.name}" + notificationLine = 4 + } + 4 -> { + messageSnippet4 = "Downloading ${track?.name}" + notificationLine = 1 + } + } Log.i(tag,"${track?.name} Download Started") + updateNotification() + + val link = "https://m.youtube.com/watch?v=shCX5YgU9yc" + var result = "" + result = link.removePrefix("https://") + result = link.removePrefix("http://") + } override fun onWaitingNetwork(download: Download) { @@ -211,13 +316,21 @@ class ForegroundService : Service(){ override fun onCompleted(download: Download) { val track = requestMap[download.request] - speed = 0 serviceScope.launch { - convertToMp3(download.file, track!!) + try{ + convertToMp3(download.file, track!!) + Log.i(tag,"${track.name} Download Completed") + }catch (e:KotlinNullPointerException + ){ + Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!") + Log.i(tag,"${track?.name} Requesting Download thru Android DM") + downloadUsingDM(download.request.url,download.request.file, track!!) + } } - Log.i(tag,"${track?.name} Download Completed") - requestMap.remove(download.request) - updateNotification() + downloaded++ + requestMap.remove(download.request) + if(requestMap.keys.toList().isEmpty()) speed = 0 + updateNotification() } override fun onDeleted(download: Download) { @@ -233,7 +346,13 @@ class ForegroundService : Service(){ } override fun onError(download: Download, error: Error, throwable: Throwable?) { + val track = requestMap[download.request] Log.i(tag,download.error.throwable.toString()) + Log.i(tag,"${track?.name} Requesting Download thru Android DM") + downloadUsingDM(download.request.url,download.request.file, track!!) + downloaded++ + requestMap.remove(download.request) + updateNotification() } override fun onPaused(download: Download) { @@ -253,12 +372,51 @@ class ForegroundService : Service(){ } + /** + * If fetch Fails , Android Download Manager To RESCUE!! + **/ + fun downloadUsingDM(url:String,outputDir:String,track: Track){ + val uri = Uri.parse(url) + val request = DownloadManager.Request(uri) + .setAllowedNetworkTypes( + DownloadManager.Request.NETWORK_WIFI or + DownloadManager.Request.NETWORK_MOBILE + ) + .setAllowedOverRoaming(false) + .setTitle(track.name) + .setDescription("Spotify Downloader Working Up here...") + .setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix( + Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator + )) + .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + //Start Download + val downloadID = downloadManager?.enqueue(request) + Log.i("DownloadManager", "Download Request Sent") + val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + //Fetching the download id received with the broadcast + val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) + //Checking if the received broadcast is for our enqueued download by matching download id + if (downloadID == id) { + convertToMp3(outputDir,track) + converted++ + //Unregister this broadcast Receiver + this@ForegroundService.unregisterReceiver(this) + } + } + } + registerReceiver(onDownloadComplete,IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + } + + /** + *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata) + **/ fun convertToMp3(filePath: String,track: Track){ val m4aFile = File(filePath) - val executionId = FFmpeg.executeAsync( - "-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}" + FFmpeg.executeAsync( + "-i $filePath -b:a 160k -vn ${filePath.substringBeforeLast('.') + ".mp3"}" ) { _, returnCode -> when (returnCode) { RETURN_CODE_SUCCESS -> { @@ -292,11 +450,11 @@ class ForegroundService : Service(){ updateNotification() //All tasks completed (REST IN PEACE) if(converted == total){ - stopForeground(false) - stopSelf() + onDestroy() } } + /** * This is the method that can be called to update the Notification */ @@ -305,15 +463,23 @@ class ForegroundService : Service(){ 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 ") + .setContentText("Total: $total Completed:$converted ") .setSubText("Speed: $speed KB/s ") .setNotificationSilent() .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.down_arrowbw) + .setStyle(NotificationCompat.InboxStyle() + .addLine(messageSnippet1) + .addLine(messageSnippet2) + .addLine(messageSnippet3) + .addLine(messageSnippet4)) .build() - mNotificationManager.notify(101, notification) + mNotificationManager.notify(notificationId, notification) } + /** + *Modifying Mp3 Tags with MetaData! + **/ private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File { val id3v1Tag = ID3v1Tag() id3v1Tag.track = track.disc_number.toString() @@ -342,10 +508,22 @@ class ForegroundService : Service(){ track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) } id3v2Tag.copyright = copyrights.joinToString() id3v2Tag.url = track.href - track.let { + track.ytCoverUrl?.let { val file = File( Environment.getExternalStorageDirectory(), - DownloadHelper.defaultDir +".Images/" + (it.album!!.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg") + DownloadHelper.defaultDir +".Images/" + it.substringAfterLast('/',it) + ".jpeg") + Log.i("Mp3Tags editing Tags",file.path) + //init array with file length + val bytesArray = ByteArray(file.length().toInt()) + val fis = FileInputStream(file) + fis.read(bytesArray) //read file into bytes[] + fis.close() + id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") + } + track.album?.let { + val file = File( + Environment.getExternalStorageDirectory(), + DownloadHelper.defaultDir +".Images/" + (it.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg") Log.i("Mp3Tags editing Tags",file.path) //init array with file length val bytesArray = ByteArray(file.length().toInt()) diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index 220b838d..18405696 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -68,7 +68,6 @@ android:layout_height="48dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" - android:layout_marginBottom="8dp" android:background="@drawable/text_background_accented" android:ems="10" android:hint="Link From Spotify" @@ -78,7 +77,6 @@ android:textColor="@color/white" android:textColorHint="@color/grey" android:textSize="19sp" - app:layout_constraintBottom_toTopOf="@+id/image_view" app:layout_constraintEnd_toStartOf="@+id/btn_search" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -103,11 +101,11 @@ android:id="@+id/image_view" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginTop="6dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="3dp" android:contentDescription="Album Cover" android:foreground="@drawable/gradient" android:padding="20dp" - android:layout_marginBottom="3dp" android:paddingBottom="10dp" android:src="@drawable/spotify_download" android:visibility="visible" @@ -115,7 +113,29 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/btn_search" /> + app:layout_constraintTop_toBottomOf="@+id/linkSearch" /> + + @@ -312,7 +332,15 @@ app:layout_constraintStart_toEndOf="@id/heart" app:layout_constraintTop_toBottomOf="@+id/developer_insta" /> - + diff --git a/app/src/main/res/xml/app_update.xml b/app/src/main/res/xml/app_update.xml index 28cc79a2..c04d3e83 100644 --- a/app/src/main/res/xml/app_update.xml +++ b/app/src/main/res/xml/app_update.xml @@ -18,8 +18,8 @@ - 1.1 - 2 + 1.2 + 3 https://github.com/Shabinder/SpotiFlyer/releases \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3243adf8..9865f31c 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ buildscript { 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" +// 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 }