Unifying All Things For Better Maintenance and Less Trouble.

This commit is contained in:
Shabinder 2020-11-09 14:11:22 +05:30
parent cac306e9e8
commit 180a284a54
23 changed files with 403 additions and 378 deletions

View File

@ -25,7 +25,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class SharedViewModel : ViewModel() {
var intentString = MutableLiveData<String>().apply { value = "" }
var intentString = MutableLiveData<String>()
var spotifyService = MutableLiveData<SpotifyService>()
private var viewModelJob = Job()

View File

@ -33,8 +33,8 @@ import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.activity
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -76,9 +76,9 @@ object DownloadHelper {
//Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent")
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
}
startService(activity,downloadList)
startService(mainActivity,downloadList)
},5000)
}
}else{
@ -121,9 +121,9 @@ object DownloadHelper {
//Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent")
sharedViewModel?.uiScope?.launch (Dispatchers.Main){
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
}
startService(activity,downloadList)
startService(mainActivity,downloadList)
},5000)
}
}

View File

@ -22,8 +22,8 @@ import android.util.Log
import android.widget.Toast
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider.activity
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.removeIllegalChars
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
@ -63,8 +63,8 @@ object YTDownloadHelper {
}
Log.i("YT Downloader Helper","Download Request Sent")
withContext(Dispatchers.Main){
Toast.makeText(activity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
startService(activity,downloadList)
Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show()
startService(mainActivity,downloadList)
}
}
}

View File

@ -40,6 +40,7 @@ data class TrackDetails(
var lyrics:String?=null,
var trackUrl:String?=null,
var albumArt: File,
var albumArtURL: String,
var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
):Parcelable

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.gaana
data class GaanaAlbum (
val tracks : List<Tracks>,
val tracks : List<GaanaTrack>,
val count : Int,
val custom_artworks : CustomArtworks,
val release_year : Int,

View File

@ -19,5 +19,5 @@ package com.shabinder.spotiflyer.models.gaana
data class GaanaArtistTracks(
val count : Int,
val tracks : List<Tracks>
val tracks : List<GaanaTrack>
)

View File

@ -24,5 +24,5 @@ data class GaanaPlaylist (
val count : Int,
val created_on : String,
val favorite_count : Int,
val tracks : List<Tracks>,
val tracks : List<GaanaTrack>,
)

View File

@ -18,5 +18,5 @@
package com.shabinder.spotiflyer.models.gaana
data class GaanaSong(
val tracks : List<Tracks>
val tracks : List<GaanaTrack>
)

View File

@ -17,9 +17,10 @@
package com.shabinder.spotiflyer.models.gaana
import com.shabinder.spotiflyer.models.DownloadStatus
import com.squareup.moshi.Json
data class Tracks (
data class GaanaTrack (
val tags : List<Tags>,
val seokey : String,
val albumseokey : String,
@ -35,4 +36,5 @@ data class Tracks (
val release_date : String,
val play_ct : String,
val secondary_language : String,
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
)

View File

@ -40,6 +40,6 @@ data class Track(
var album: Album? = null,
var external_ids: Map<String?, String?>? = null,
var popularity: Int? = null,
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
):Parcelable

View File

@ -1,125 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.recyclerView
import android.annotation.SuppressLint
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track
import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.activity
import kotlinx.coroutines.launch
import java.io.File
class SpotifyTrackListAdapter(private val spotifyViewModel : SpotifyViewModel): ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(SpotifyTrackDiffCallback()) {
var isAlbum:Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TrackListItemBinding.inflate(layoutInflater,parent,false)
return ViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
if(itemCount ==1 || isAlbum){
holder.binding.imageUrl.visibility = View.GONE}else{
spotifyViewModel.uiScope.launch {
//Placeholder Set
bindImage(holder.binding.imageUrl, item.album?.images?.get(0)?.url, Source.Spotify)
}
}
holder.binding.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
holder.binding.artist.text = "${item.artists?.get(0)?.name?:""}..."
holder.binding.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
when (item.downloaded) {
DownloadStatus.Downloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick)
holder.binding.btnDownload.clearAnimation()
}
DownloadStatus.Downloading -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(holder.binding.btnDownload)
}
DownloadStatus.NotDownloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow)
holder.binding.btnDownload.clearAnimation()
holder.binding.btnDownload.setOnClickListener{
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
Toast.makeText(activity,"Processing!",Toast.LENGTH_SHORT).show()
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading
spotifyViewModel.uiScope.launch {
val itemList = mutableListOf<TrackDetails>()
itemList.add(item.let { track ->
val artistsList = mutableListOf<String>()
track.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
TrackDetails(
title = track.name.toString(),
artists = artistsList,
durationSec = (track.duration_ms/1000).toInt(),
albumArt = File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (track.album?.images?.get(0)?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = track.album?.name,
year = track.album?.release_date,
comment = "Genres:${track.album?.genres?.joinToString()}",
trackUrl = track.href,
source = Source.Spotify
)
}
)
downloadAllTracks(spotifyViewModel.folderType,spotifyViewModel.subFolder,itemList)
}
notifyItemChanged(position)//start showing anim!
}
}
}
}
class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root)
}
class SpotifyTrackDiffCallback: DiffUtil.ItemCallback<Track>(){
override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean {
return oldItem == newItem //Downloaded Check
}
}

View File

@ -22,39 +22,35 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.youtube.YoutubeViewModel
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch
class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
class TrackListAdapter(private val viewModel :BaseViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()) {
var source:Source =Source.Spotify
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SpotifyTrackListAdapter.ViewHolder {
): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TrackListItemBinding.inflate(layoutInflater,parent,false)
// val view = layoutInflater.inflate(R.layout.track_list_item,parent,false)
return SpotifyTrackListAdapter.ViewHolder(binding)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: SpotifyTrackListAdapter.ViewHolder, position: Int) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
if(itemCount == 1){
holder.binding.imageUrl.visibility = View.GONE}else{
youtubeViewModel.uiScope.launch {
bindImage(holder.binding.imageUrl,
"https://i.ytimg.com/vi/${item.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/hqdefault.jpg"
,
Source.YouTube
)
if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{
viewModel.uiScope.launch {
bindImage(holder.binding.imageUrl,item.albumArtURL, source)
}
}
@ -79,15 +75,26 @@ class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): L
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading
youtubeViewModel.uiScope.launch {
val itemList = mutableListOf<TrackDetails>()
itemList.add(item)
YTDownloadHelper.downloadYTTracks(
youtubeViewModel.folderType,
youtubeViewModel.subFolder,
itemList
when(source){
Source.Spotify -> {
viewModel.uiScope.launch {
DownloadHelper.downloadAllTracks(
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
}
}
Source.YouTube -> {
viewModel.uiScope.launch {
YTDownloadHelper.downloadYTTracks(
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
}
}
}
notifyItemChanged(position)//start showing anim!
}
}
@ -97,8 +104,15 @@ class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): L
holder.binding.artist.text = "${item.artists.get(0)}..."
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec"
}
class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root)
fun submitList(list: MutableList<TrackDetails>?, source: Source) {
super.submitList(list)
this.source = source
}
class YouTubeTrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){
}
class TrackDiffCallback: DiffUtil.ItemCallback<TrackDetails>(){
override fun areItemsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean {
return oldItem.title == newItem.title
}

View File

@ -20,6 +20,7 @@ package com.shabinder.spotiflyer.ui.gaana
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -29,8 +30,14 @@ import androidx.lifecycle.ViewModelProvider
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class GaanaFragment : Fragment() {
@ -39,6 +46,7 @@ class GaanaFragment : Fragment() {
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
private lateinit var viewModel: GaanaViewModel
private lateinit var adapter: TrackListAdapter
@Inject lateinit var gaanaInterface: GaanaInterface
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
@ -49,6 +57,63 @@ class GaanaFragment : Fragment() {
): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.track_list_fragment, container, false)
viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
adapter = TrackListAdapter(viewModel)
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
//Link Schema: https://gaana.com/type/link
val link = gaanaLink.substringAfterLast('/', "error")
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
Log.i("Gaana Fragment", "$type : $link")
when{
type == "Error" || link == "Error" -> {
showMessage("Please Check Your Link!")
Provider.mainActivity.onBackPressed()
}
else -> {
viewModel.gaanaSearch(type,link)
binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE
rotateAnim(binding.downloadingFab)
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>()
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("spotify")
loadAllImages(
requireActivity(),
urlList
)
}
viewModel.uiScope.launch {
val finalList = viewModel.trackList.value
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks(
viewModel.folderType,
viewModel.subFolder,
finalList ?: listOf(),
)
}
}
}
}
return binding.root
}
}

View File

@ -18,7 +18,18 @@
package com.shabinder.spotiflyer.ui.gaana
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.utils.BaseViewModel
class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : ViewModel()
class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : BaseViewModel(){
override var folderType:String = ""
override var subFolder:String = ""
var gaanaInterface : GaanaInterface? = null
fun gaanaSearch(type:String,link:String){
}
}

View File

@ -65,19 +65,60 @@ class MainFragment : Fragment() {
return@setOnClickListener
}
val link = binding.linkSearch.text.toString()
if (link.contains("spotify",true)){
when{
//SPOTIFY
link.contains("spotify",true) -> {
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
(activity as MainActivity).authenticateSpotify()
}
findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link))
}else if(link.contains("youtube.com",true) || link.contains("youtu.be",true) ){
}
//YOUTUBE
link.contains("youtube.com",true) || link.contains("youtu.be",true) -> {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
}else showMessage("Link is Not Valid",true)
}
//GAANA
link.contains("gaana",true) -> {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToGaanaFragment(link))
}
else -> showMessage("Link is Not Valid",true)
}
}
handleIntent()
return binding.root
}
/**
* Handle Intent If there is any!
**/
private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
sharedViewModel.uiScope.launch(Dispatchers.IO) {
//Wait for any Authentication to Finish ,
// this Wait prevents from multiple Authentication Requests
Thread.sleep(1000)
if(sharedViewModel.spotifyService.value == null){
//Not Authenticated Yet
Provider.mainActivity.authenticateSpotify()
while (sharedViewModel.spotifyService.value == null) {
//Waiting for Authentication to Finish
Thread.sleep(1000)
}
}
withContext(Dispatchers.Main){
binding.linkSearch.setText(sharedViewModel.intentString.value)
binding.btnSearch.performClick()
//Intent Consumed
sharedViewModel.intentString.value = null
}
}
}
})
}
private fun initializeAll() {
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
@ -94,44 +135,14 @@ class MainFragment : Fragment() {
}
}
/**
* Implementing buttons
**/
private fun historyButton() {
binding.btnHistory.setOnClickListener {
findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord())
}
}
/**
* Handle Intent If there is any!
**/
private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{
if(it != ""){
sharedViewModel.uiScope.launch(Dispatchers.IO) {
//Wait for any Authentication to Finish ,
// this Wait prevents from multiple Authentication Requests
Thread.sleep(1000)
if(sharedViewModel.spotifyService.value == null){
//Not Authenticated Yet
Provider.activity.authenticateSpotify()
while (sharedViewModel.spotifyService.value == null) {
//Waiting for Authentication to Finish
Thread.sleep(1000)
}
}
withContext(Dispatchers.Main){
binding.linkSearch.setText(sharedViewModel.intentString.value)
binding.btnSearch.performClick()
sharedViewModel.intentString.value = ""
}
}
}
})
}
/**
* Implementing buttons
**/
private fun openSpotifyButton() {
val manager: PackageManager = requireActivity().packageManager
try {

View File

@ -23,7 +23,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@ -41,13 +40,12 @@ import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@Suppress("DEPRECATION")
@ -58,7 +56,7 @@ class SpotifyFragment : Fragment() {
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
private lateinit var viewModel: SpotifyViewModel
private lateinit var adapter:SpotifyTrackListAdapter
private lateinit var adapter:TrackListAdapter
private var intentFilter:IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
@ -73,26 +71,29 @@ class SpotifyFragment : Fragment() {
initializeLiveDataObservers()
initializeBroadcast()
val args = SpotifyFragmentArgs.fromBundle(requireArguments())
val spotifyLink = args.link
val spotifyLink = SpotifyFragmentArgs.fromBundle(requireArguments()).link.substringAfter("open.spotify.com/")
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
Log.i("Fragment", "$type : $link")
Log.i("Spotify Fragment", "$type : $link")
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
(activity as MainActivity).authenticateSpotify()
}
if (type == "Error" || link == "Error") {//Incorrect Link
when{
type == "Error" || link == "Error" -> {
showMessage("Please Check Your Link!")
}else if(spotifyLink.contains("open.spotify",true)){//Link Validation!!
mainActivity.onBackPressed()
}
else -> {
if(type == "episode" || type == "show"){//TODO Implementation
showMessage("Implementing Soon, Stay Tuned!")
}
else{
viewModel.spotifySearch(type,link)
if(type=="album")adapter.isAlbum = true
binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){
@ -112,7 +113,7 @@ class SpotifyFragment : Fragment() {
showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>()
viewModel.trackList.value?.forEach { urlList.add(it.album?.images?.get(0)?.url.toString()) }
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("spotify")
loadAllImages(
@ -121,23 +122,7 @@ class SpotifyFragment : Fragment() {
)
}
viewModel.uiScope.launch {
val finalList = viewModel.trackList.value?.map{
val artistsList = mutableListOf<String>()
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
TrackDetails(
title = it.name.toString(),
artists = artistsList,
durationSec = (it.duration_ms/1000).toInt(),
albumArt = File(
Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + (it.album?.images?.get(0)?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name,
year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href,
source = Source.Spotify
)
}
val finalList = viewModel.trackList.value
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks(
viewModel.folderType,
@ -148,6 +133,8 @@ class SpotifyFragment : Fragment() {
}
}
}
}
return binding.root
}
@ -160,7 +147,7 @@ class SpotifyFragment : Fragment() {
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
viewModel.spotifyService = it
})
adapter = SpotifyTrackListAdapter(viewModel)
adapter = TrackListAdapter(viewModel)
DownloadHelper.youtubeMusicApi = youtubeMusicApi
DownloadHelper.sharedViewModel = sharedViewModel
DownloadHelper.statusBar = binding.statusBar
@ -211,7 +198,7 @@ class SpotifyFragment : Fragment() {
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = viewModel.trackList.value?.map { it.name }?.indexOf(trackDetails.title) ?: -1
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
Log.i("Track","Download Completed Intent :$position")
if(position != -1) {
val track = viewModel.trackList.value?.get(position)

View File

@ -17,34 +17,30 @@
package com.shabinder.spotiflyer.ui.spotify
import android.os.Environment
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.*
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.BaseViewModel
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) :
ViewModel(){
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : BaseViewModel(){
override var folderType:String = ""
override var subFolder:String = ""
var folderType:String = ""
var subFolder:String = ""
var trackList = MutableLiveData<MutableList<Track>>()
private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = loading }
var coverUrl = MutableLiveData<String>().apply { value = loading }
var spotifyService : SpotifyService? = null
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun spotifySearch(type:String,link: String){
when (type) {
"track" -> {
@ -56,7 +52,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
trackObject.downloaded = DownloadStatus.Downloaded
}
tempTrackList.add(trackObject)
trackList.value = tempTrackList
trackList.value = tempTrackList.toTrackDetailsList()
title.value = trackObject.name
coverUrl.value = trackObject.album!!.images?.get(0)!!.url!!
withContext(Dispatchers.IO){
@ -86,7 +82,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url)))
tempTrackList.add(it)
}
trackList.value = tempTrackList
trackList.value = tempTrackList.toTrackDetailsList()
title.value = albumObject?.name
coverUrl.value = albumObject?.images?.get(0)?.url
withContext(Dispatchers.IO){
@ -129,7 +125,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
}
Log.i("Total Tracks Fetched",tempTrackList.size.toString())
trackList.value = tempTrackList
trackList.value = tempTrackList.toTrackDetailsList()
title.value = playlistObject?.name
coverUrl.value = playlistObject?.images?.get(0)?.url.toString()
withContext(Dispatchers.IO){
@ -152,6 +148,26 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
}
}
private fun List<Track>.toTrackDetailsList() = this.map {
val artistsList = mutableListOf<String>()
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
TrackDetails(
title = it.name.toString(),
artists = artistsList,
durationSec = (it.duration_ms/1000).toInt(),
albumArt = File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (it.album?.images?.get(0)?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name,
year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href,
downloaded = it.downloaded,
source = Source.Spotify,
albumArtURL = it.album?.images?.get(0)?.url.toString()
)
}.toMutableList()
private suspend fun getTrackDetails(trackLink:String): Track?{
Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink")
return spotifyService?.getTrack(trackLink)?.value
@ -168,10 +184,4 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit")
return spotifyService?.getPlaylistTracks(link, offset, limit)?.value
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}

View File

@ -37,7 +37,7 @@ import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@ -51,7 +51,7 @@ class YoutubeFragment : Fragment() {
private lateinit var viewModel: YoutubeViewModel
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var ytDownloader: YoutubeDownloader
private lateinit var adapter : YoutubeTrackListAdapter
private lateinit var adapter : TrackListAdapter
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
private val sampleDomain2 = "youtu.be"
@ -64,7 +64,7 @@ class YoutubeFragment : Fragment() {
binding = DataBindingUtil.inflate(inflater,R.layout.track_list_fragment,container,false)
viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
adapter = YoutubeTrackListAdapter(viewModel)
adapter = TrackListAdapter(viewModel)
binding.trackList.adapter = adapter
initializeLiveDataObservers()
@ -108,16 +108,16 @@ class YoutubeFragment : Fragment() {
rotateAnim(binding.downloadingFab)
for (track in viewModel.ytTrackList.value?: listOf()){
for (track in viewModel.trackList.value?: listOf()){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading
adapter.notifyItemChanged(viewModel.ytTrackList.value!!.indexOf(track))
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>()
viewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
viewModel.trackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/hqdefault.jpg")}
//Appending Source
urlList.add("youtube")
@ -130,7 +130,7 @@ class YoutubeFragment : Fragment() {
YTDownloadHelper.downloadYTTracks(
type = viewModel.folderType,
subFolder = viewModel.subFolder,
tracks = viewModel.ytTrackList.value ?: listOf()
tracks = viewModel.trackList.value ?: listOf()
)
}
}
@ -150,13 +150,13 @@ class YoutubeFragment : Fragment() {
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = viewModel.ytTrackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
Log.i("Track","Download Completed Intent :$position")
if(position != -1) {
val track = viewModel.ytTrackList.value?.get(position)
val track = viewModel.trackList.value?.get(position)
track?.let{
it.downloaded = DownloadStatus.Downloaded
viewModel.ytTrackList.value?.set(position, it)
viewModel.trackList.value?.set(position, it)
adapter.notifyItemChanged(position)
checkIfAllDownloaded()
}
@ -174,7 +174,7 @@ class YoutubeFragment : Fragment() {
}
private fun checkIfAllDownloaded() {
if(!viewModel.ytTrackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
if(!viewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
//All Tracks Downloaded
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
@ -195,7 +195,7 @@ class YoutubeFragment : Fragment() {
/**
* TrackList Binding Observer!
**/
viewModel.ytTrackList.observe(viewLifecycleOwner, {
viewModel.trackList.observe(viewLifecycleOwner, {
adapter.submitList(it)
})

View File

@ -21,38 +21,31 @@ import android.annotation.SuppressLint
import android.os.Environment
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.BaseViewModel
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.removeIllegalChars
import com.shabinder.spotiflyer.utils.showMessage
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : ViewModel(){
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : BaseViewModel(){
/*
* YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
* */
val ytTrackList = MutableLiveData<MutableList<TrackDetails>>()
private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
var coverUrl = MutableLiveData<String>().apply { value = loading }
val folderType = "YT_Downloads"
var subFolder = ""
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override var folderType = "YT_Downloads"
override var subFolder = ""
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){
try{
@ -67,7 +60,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
)
ytTrackList.postValue(videos.map {
this@YoutubeViewModel.trackList.postValue(videos.map {
TrackDetails(
title = it.title(),
artists = listOf(it.author().toString()),
@ -77,13 +70,13 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
defaultDir + ".Images/" + it.videoId() + ".jpeg"
),
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = it.title(),
type = folderType,
subFolder = subFolder
)
).exists()
)).exists()
)
DownloadStatus.Downloaded
else {
@ -120,7 +113,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
val detail = video?.details()
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
Log.i("YT View Model",detail.toString())
ytTrackList.postValue(
this@YoutubeViewModel.trackList.postValue(
listOf(
TrackDetails(
title = name,
@ -128,9 +121,10 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
durationSec = detail?.lengthSeconds()?:0,
albumArt = File(
Environment.getExternalStorageDirectory(),
defaultDir +".Images/" + searchId + ".jpeg"
"$defaultDir.Images/$searchId.jpeg"
),
source = Source.YouTube
source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
)
).toMutableList()
)

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.utils
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.TrackDetails
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
abstract class BaseViewModel:ViewModel() {
abstract var folderType:String
abstract var subFolder:String
open val trackList = MutableLiveData<MutableList<TrackDetails>>()
private val viewModelJob:CompletableJob = Job()
open val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob)
private val loading = "Loading!"
open var title = MutableLiveData<String>().apply { value = loading }
open var coverUrl = MutableLiveData<String>().apply { value = loading }
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}

View File

@ -49,7 +49,7 @@ import javax.inject.Singleton
@Module
object Provider {
val activity: MainActivity = MainActivity.getInstance()
val mainActivity: MainActivity = MainActivity.getInstance()
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
@ -68,7 +68,7 @@ object Provider {
@Provides
@Singleton
fun provideUpi():EasyUpiPayment {
return EasyUpiPayment.Builder(activity)
return EasyUpiPayment.Builder(mainActivity)
.setPayeeVpa("technoshab@paytm")
.setPayeeName("Shabinder Singh")
.setTransactionId("UNIQUE_TRANSACTION_ID")

View File

@ -41,13 +41,12 @@ import com.google.android.material.snackbar.Snackbar
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider.activity
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.worker.ForegroundService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
@ -77,7 +76,7 @@ fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,e
fun isOnline(): Boolean {
var result = false
val connectivityManager =
activity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
@ -90,7 +89,7 @@ fun isOnline(): Boolean {
}
} else {
val netInfo =
(activity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
(mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
result = netInfo != null && netInfo.isConnected
}
}
@ -100,12 +99,12 @@ fun isOnline(): Boolean {
fun showMessage(message: String, long: Boolean = false){
CoroutineScope(Dispatchers.Main).launch{
Snackbar.make(
activity.snackBarAnchor,
mainActivity.snackBarAnchor,
message,
if (long) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
).also { snackbar ->
snackbar.setAction("Ok") {
snackbar.dismiss()
).apply {
setAction("Ok") {
dismiss()
}
}.show()
}
@ -126,7 +125,7 @@ fun rotateAnim(view: View){
fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch {
activity.apply {
mainActivity.apply {
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text))
@ -187,13 +186,10 @@ fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
}
// the File to save , append increasing numeric counter to prevent files from getting overwritten.
resource?.copyTo(file)
withContext(Dispatchers.Main){
Glide.with(imgView)
.load(file)
.placeholder(R.drawable.ic_song_placeholder)
.into(imgView)
// Log.i("Glide","imageSaved")
}
} catch (e: IOException) {
e.printStackTrace()
}

View File

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