diff --git a/.idea/dictionaries/shabinder.xml b/.idea/dictionaries/shabinder.xml index 6457524e..739b47bf 100755 --- a/.idea/dictionaries/shabinder.xml +++ b/.idea/dictionaries/shabinder.xml @@ -6,6 +6,7 @@ emoji ffmpeg flyer + hqdefault insta instagram jetbrains diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt index 02e17f8f..523447d4 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt @@ -48,8 +48,8 @@ object YTDownloadHelper { val downloadObject = DownloadObject( trackDetails = it, - ytVideoId = "https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/") - .substringBeforeLast(".")}/maxresdefault.jpg", + ytVideoId = it.albumArt.absolutePath.substringAfterLast("/") + .substringBeforeLast("."), outputFile = outputFile ) diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt index 696ad40d..670abecf 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt @@ -44,7 +44,7 @@ class DownloadRecordAdapter: ListAdapter(YouTubeTrackDiffCallback()) { - - private val adapterScope = CoroutineScope(Dispatchers.Default) +class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): ListAdapter(YouTubeTrackDiffCallback()) { override fun onCreateViewHolder( parent: ViewGroup, @@ -48,24 +51,50 @@ class YoutubeTrackListAdapter: ListAdapter { + holder.binding.btnDownload.setImageResource(R.drawable.ic_tick) + holder.binding.btnDownload.clearAnimation() + } + DownloadStatus.Downloading -> { + holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) + rotateAnim(holder.binding.btnDownload) + } + DownloadStatus.NotDownloaded -> { + holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow) + holder.binding.btnDownload.clearAnimation() + holder.binding.btnDownload.setOnClickListener{ + Toast.makeText(Provider.activity,"Processing!", Toast.LENGTH_SHORT).show() + holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) + rotateAnim(it) + item.downloaded = DownloadStatus.Downloading + youtubeViewModel.uiScope.launch { + val itemList = mutableListOf() + itemList.add(item) + YTDownloadHelper.downloadYTTracks( + youtubeViewModel.folderType, + youtubeViewModel.subFolder, + itemList + ) + } + notifyItemChanged(position)//start showing anim! + } + } + } + holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}" holder.binding.artist.text = "${item.artists.get(0)}..." holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec" - holder.binding.btnDownload.setOnClickListener{ - adapterScope.launch { -// YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format) - } - } } } class YouTubeTrackDiffCallback: DiffUtil.ItemCallback(){ diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt index 9801997a..7f382b40 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt @@ -17,7 +17,12 @@ package com.shabinder.spotiflyer.ui.youtube +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -52,6 +57,8 @@ class YoutubeFragment : Fragment() { private lateinit var adapter : YoutubeTrackListAdapter private val sampleDomain1 = "youtube.com" private val sampleDomain2 = "youtu.be" + private var intentFilter: IntentFilter? = null + private var updateUIReceiver: BroadcastReceiver? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -60,10 +67,11 @@ class YoutubeFragment : Fragment() { binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false) youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) - adapter = YoutubeTrackListAdapter() + adapter = YoutubeTrackListAdapter(youtubeViewModel) binding.trackList.adapter = adapter initializeLiveDataObservers() + initializeBroadcast() val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val link = args.link @@ -89,6 +97,10 @@ class YoutubeFragment : Fragment() { youtubeViewModel.getYTTrack(searchId,ytDownloader) }else{showToast("Your Youtube Link is not of a Video!!")} } + + /* + * Download All Tracks + * */ binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.visibility = View.GONE binding.downloadingFab.visibility = View.VISIBLE @@ -105,7 +117,7 @@ class YoutubeFragment : Fragment() { sharedViewModel.uiScope.launch(Dispatchers.Default){ val urlList = arrayListOf() youtubeViewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/") - .substringBeforeLast(".")}/maxresdefault.jpg")} + .substringBeforeLast(".")}/hqdefault.jpg")} //Appending Source urlList.add("youtube") loadAllImages( @@ -122,7 +134,56 @@ class YoutubeFragment : Fragment() { } } } + override fun onResume() { + super.onResume() + initializeBroadcast() + } + private fun initializeBroadcast() { + intentFilter = IntentFilter() + intentFilter?.addAction("track_download_completed") + + updateUIReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + //UI update here + if (intent != null){ + val trackDetails = intent.getParcelableExtra("track") + trackDetails?.let { + val position: Int = youtubeViewModel.ytTrackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1 + Log.i("Track","Download Completed Intent :$position") + if(position != -1) { + val track = youtubeViewModel.ytTrackList.value?.get(position) + track?.let{ + it.downloaded = DownloadStatus.Downloaded + youtubeViewModel.ytTrackList.value?.set(position, it) + adapter.notifyItemChanged(position) + checkIfAllDownloaded() + } + } + } + } + } + } + requireActivity().registerReceiver(updateUIReceiver, intentFilter) + } + + override fun onPause() { + super.onPause() + requireActivity().unregisterReceiver(updateUIReceiver) + } + + private fun checkIfAllDownloaded() { + if(!youtubeViewModel.ytTrackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){ + //All Tracks Downloaded + binding.btnDownloadAll.visibility = View.GONE + binding.downloadingFab.apply{ + setImageResource(R.drawable.ic_tick) + visibility = View.VISIBLE + clearAnimation() + keepScreenOn = false + } + } + } private fun initializeLiveDataObservers() { /** * CoverUrl Binding Observer! diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt index f9a7b15c..dedfb6e0 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt @@ -32,6 +32,7 @@ import com.shabinder.spotiflyer.models.Source import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.utils.Provider.defaultDir +import com.shabinder.spotiflyer.utils.Provider.showToast import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.removeIllegalChars import kotlinx.coroutines.* @@ -41,10 +42,11 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO /* * YT Album Art Schema - * Normal Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" + * HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" + * Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg" * */ - val ytTrackList = MutableLiveData>() + val ytTrackList = MutableLiveData>() val format = MutableLiveData() private val loading = "Loading" var title = MutableLiveData().apply { value = "\"Loading!\"" } @@ -56,81 +58,101 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){ - uiScope.launch(Dispatchers.IO) { - Log.i("YT Playlist",searchId) - val playlist = ytDownloader.getPlaylist(searchId) - val playlistDetails = playlist.details() - val name = playlistDetails.title() - subFolder = removeIllegalChars(name).toString() - val videos = playlist.videos() - coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg") - title.postValue( - if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} - ) - ytTrackList.postValue(videos.map { - TrackDetails( - title = it.title(), - artists = listOf(it.author().toString()), - durationSec = it.lengthSeconds(), - albumArt = File( - Environment.getExternalStorageDirectory(), - defaultDir +".Images/" + it.videoId() + ".jpeg" - ), - source = Source.YouTube, - downloaded = if(File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()) - DownloadStatus.Downloaded - else DownloadStatus.NotDownloaded + try{ + uiScope.launch(Dispatchers.IO) { + Log.i("YT Playlist",searchId) + val playlist = ytDownloader.getPlaylist(searchId) + val playlistDetails = playlist.details() + val name = playlistDetails.title() + subFolder = removeIllegalChars(name).toString() + val videos = playlist.videos() + coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg") + title.postValue( + if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} ) - }) + ytTrackList.postValue(videos.map { + TrackDetails( + title = it.title(), + artists = listOf(it.author().toString()), + durationSec = it.lengthSeconds(), + albumArt = File( + Environment.getExternalStorageDirectory(), + defaultDir + ".Images/" + it.videoId() + ".jpeg" + ), + source = Source.YouTube, + downloaded = if (File( + finalOutputDir( + itemName = it.title(), + type = folderType, + subFolder = subFolder + ) + ).exists() + ) + DownloadStatus.Downloaded + else { + DownloadStatus.NotDownloaded + } + ) + }.toMutableList()) withContext(Dispatchers.IO){ - databaseDAO.insert(DownloadRecord( - type = "PlayList", - name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, - link = "https://www.youtube.com/playlist?list=$searchId", - coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/maxresdefault.jpg", - totalFiles = videos.size, - directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder), - downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists() - )) + databaseDAO.insert(DownloadRecord( + type = "PlayList", + name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, + link = "https://www.youtube.com/playlist?list=$searchId", + coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg", + totalFiles = videos.size, + directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder), + downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists() + )) + } } + }catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){ + showToast("An Error Occurred While Processing!") } + } @SuppressLint("DefaultLocale") fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) { - uiScope.launch(Dispatchers.IO) { - Log.i("YT Video",searchId) - val video = ytDownloader.getVideo(searchId) - coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg") - val detail = video?.details() - val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: "" - Log.i("YT View Model",detail.toString()) - ytTrackList.postValue(listOf(TrackDetails( - title = name, - artists = listOf(detail?.author().toString()), - durationSec = detail?.lengthSeconds()?:0, - albumArt = File( - Environment.getExternalStorageDirectory(), - Provider.defaultDir +".Images/" + searchId + ".jpeg" - ), - source = Source.YouTube - ))) - title.postValue( - if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} - ) + try{ + uiScope.launch(Dispatchers.IO) { + Log.i("YT Video",searchId) + val video = ytDownloader.getVideo(searchId) + coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg") + val detail = video?.details() + val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: "" + Log.i("YT View Model",detail.toString()) + ytTrackList.postValue( + listOf(TrackDetails( + title = name, + artists = listOf(detail?.author().toString()), + durationSec = detail?.lengthSeconds()?:0, + albumArt = File( + Environment.getExternalStorageDirectory(), + Provider.defaultDir +".Images/" + searchId + ".jpeg" + ), + source = Source.YouTube + )).toMutableList() + ) + title.postValue( + if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} + ) - withContext(Dispatchers.IO){ - databaseDAO.insert(DownloadRecord( - type = "Track", - name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, - link = "https://www.youtube.com/watch?v=$searchId", - coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg", - totalFiles = 1, - downloaded = false, - directory = finalOutputDir(type = "YT_Downloads") - )) + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "Track", + name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, + link = "https://www.youtube.com/watch?v=$searchId", + coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", + totalFiles = 1, + downloaded = false, + directory = finalOutputDir(type = "YT_Downloads") + )) + } } + } catch (e:com.github.kiulian.downloader.YoutubeException){ + showToast("An Error Occurred While Processing!") } } } diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt index 123a2fd2..84863e5f 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt @@ -19,6 +19,7 @@ package com.shabinder.spotiflyer.utils import android.content.Context import android.os.Environment +import android.widget.Toast import com.github.kiulian.downloader.YoutubeDownloader import com.shabinder.spotiflyer.App import com.shabinder.spotiflyer.MainActivity @@ -117,4 +118,7 @@ object Provider { return retrofit.create(YoutubeMusicApi::class.java) } + fun showToast(string: String,long:Boolean=false){ + Toast.makeText(activity,string,if(long)Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() + } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt index 8435cb1e..77c84308 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt @@ -28,7 +28,6 @@ import android.view.animation.RotateAnimation import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.core.net.toUri -import androidx.databinding.BindingAdapter import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -77,8 +76,7 @@ fun rotateAnim(view: View){ view.animation = rotate } -@BindingAdapter("imageUrl") -fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) { +fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) { imgUrl?.let { val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() Glide @@ -122,6 +120,10 @@ fun bindImage(imgView: ImageView, imgUrl: String?,source: Source) { defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg" ) } + else -> File( + Environment.getExternalStorageDirectory(), + defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg" + ) } // the File to save , append increasing numeric counter to prevent files from getting overwritten. resource?.copyTo(file)