Network Connection Change Handling Improved,i.e,Less Crashes and Gaana Implementation Also Started

This commit is contained in:
Shabinder 2020-11-09 01:47:26 +05:30
parent a5793cc72c
commit c489c8c84a
61 changed files with 977 additions and 564 deletions

View File

@ -1,11 +1,16 @@
<component name="ProjectDictionaryState">
<dictionary name="shabinder">
<words>
<w>albumseokey</w>
<w>amita</w>
<w>cardview</w>
<w>cherrypick</w>
<w>downloadrecord</w>
<w>emoji</w>
<w>ffmpeg</w>
<w>flyer</w>
<w>gaana</w>
<w>gener</w>
<w>hqdefault</w>
<w>insta</w>
<w>instagram</w>
@ -19,8 +24,10 @@
<w>musicplaceholder</w>
<w>raleway</w>
<w>semibold</w>
<w>seokey</w>
<w>shabinder</w>
<w>singh</w>
<w>snackbar</w>
<w>spoti</w>
<w>spotiflyer</w>
<w>spotify</w>

View File

@ -21,7 +21,7 @@ apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'dagger.hilt.android.plugin'
//apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlinx-serialization'
android {
compileSdkVersion 29
@ -90,8 +90,10 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
implementation "androidx.room:room-runtime:2.2.5"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-ktx:2.2.5"
implementation "com.google.dagger:hilt-android:$hilt_version"
@ -116,7 +118,6 @@ dependencies {
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.beust:klaxon:5.4'
implementation 'me.xdrop:fuzzywuzzy:1.3.1'

View File

@ -11,7 +11,25 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
-keepclassmembers class kotlinx.serialization.json.* {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.* {
kotlinx.serialization.KSerializer serializer(...);
}
# Change here com.yourcompany.yourpackage
-keep,includedescriptorclasses class com.shabinder.spotiflyer.**$$serializer { *; } # <-- change package name to your app's
-keepclassmembers class com.shabinder.spotiflyer* { # <-- change package name to your app's
*** Companion;
}
-keepclasseswithmembers class com.shabinder.spotiflyer.* { # <-- change package name to your app's
kotlinx.serialization.KSerializer serializer(...);
}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

View File

@ -28,6 +28,7 @@ import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.databinding.DataBindingUtil
@ -35,10 +36,12 @@ import androidx.lifecycle.ViewModelProvider
import com.github.javiersantos.appupdater.AppUpdater
import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.shabinder.spotiflyer.databinding.MainActivityBinding
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.NetworkInterceptor
import com.shabinder.spotiflyer.utils.Provider.activity
import com.shabinder.spotiflyer.utils.SpotifyService
import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.createDirectories
import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.startService
import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint
@ -54,35 +57,29 @@ import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity(){
private var spotifyService : SpotifyService? = null
private var isConnected: Boolean = false
private var sharedPref :SharedPreferences? = null
private var token :String =""
private lateinit var binding: MainActivityBinding
lateinit var snackBarAnchor: View
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
@Inject lateinit var moshi: Moshi
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
snackBarAnchor = binding.snackBarPosition
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
//Enabling Dark Mode
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
if(sharedViewModel.spotifyService.value == null){
authenticateSpotify()
}else{
implementSpotifyService(sharedViewModel.accessToken.value!!)
}
requestPermission()
disableDozeMode()
checkIfLatestVersion()
createDirectories()
isConnected = sharedViewModel.isOnline(this)
sharedViewModel.isConnected.value = isConnected
Log.i("Connection Status", isConnected.toString())
Log.i("Connection Status", isOnline().toString())
//starting Notification and Downloader Service!
startService(this)
@ -140,7 +137,7 @@ class MainActivity : AppCompatActivity(){
"Bearer $token"
).build()
chain.proceed(request)
})
}).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder()
.baseUrl("https://api.spotify.com/v1/")
@ -155,16 +152,12 @@ class MainActivity : AppCompatActivity(){
fun authenticateSpotify() {
sharedViewModel.uiScope.launch {
if (isConnected) {
Log.i("Post Request", "Made")
token = spotifyServiceTokenRequest.getToken()!!.access_token
implementSpotifyService(token)
Log.i("Post Request", token)
sharedViewModel.accessToken.value = token
}else{
Log.i("network", "unavailable")
// sharedViewModel.showAlertDialog(resources,this@MainActivity)
Log.i("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken()
token.value?.let {
implementSpotifyService(it.access_token)
}
Log.i("Spotify Token", token.value.toString())
}
}
@ -189,19 +182,6 @@ class MainActivity : AppCompatActivity(){
}
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
savedInstanceState.putString("token", token)
super.onSaveInstanceState(savedInstanceState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
if (savedInstanceState.getString("token") ==""){
super.onRestoreInstanceState(savedInstanceState)
}else{
implementSpotifyService(savedInstanceState.getString("token")!!)
super.onRestoreInstanceState(savedInstanceState)
}
}
private fun checkIfLatestVersion() {
val appUpdater = AppUpdater(this)
.showAppUpdated(false)//true:Show App is Update Dialog
@ -220,14 +200,7 @@ class MainActivity : AppCompatActivity(){
appUpdater.start()
}
companion object{
private var instance = MainActivity()
fun getInstance():MainActivity{
return instance
}
}
init {
instance = this
activity = this
}
}

View File

@ -17,49 +17,22 @@
package com.shabinder.spotiflyer
import android.content.Context
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Environment
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.shabinder.spotiflyer.utils.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.io.File
class SharedViewModel : ViewModel() {
var intentString = MutableLiveData<String>().apply { value = "" }
var spotifyService = MutableLiveData<SpotifyService>()
var accessToken = MutableLiveData<String>().apply { value = "" }
var isConnected = MutableLiveData<Boolean>().apply { value = false }
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
fun showAlertDialog(resources:Resources,context: Context){
MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text))
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
// Respond to neutral button press
}
.show()
}
fun isOnline(context: Context): Boolean {
val cm =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val netInfo = cm.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
}
}

View File

@ -27,7 +27,11 @@ import android.view.animation.Animation
import android.widget.TextView
import android.widget.Toast
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.models.*
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.DownloadStatus
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
@ -39,7 +43,7 @@ import retrofit2.Callback
import retrofit2.Response
import java.io.File
object SpotifyDownloadHelper {
object DownloadHelper {
var statusBar:TextView? = null
var youtubeMusicApi: YoutubeMusicApi? = null
@ -55,12 +59,16 @@ object SpotifyDownloadHelper {
suspend fun downloadAllTracks(
type:String,
subFolder: String?,
trackList: List<Track>) {
trackList: List<TrackDetails>) {
resetStatusBar()// For New Download Request's Status
val downloadList = ArrayList<DownloadObject>()
withContext(Dispatchers.Main){
total += trackList.size // Adding New Download List Count to StatusBar
trackList.forEachIndexed { index, it ->
if(!isOnline()){
showNoConnectionAlert()
return@withContext
}
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
processed++
if(index == (trackList.size-1)){//LastElement
@ -74,49 +82,32 @@ object SpotifyDownloadHelper {
},5000)
}
}else{
val artistsList = mutableListOf<String>()
it.artists?.forEach { artist -> artistsList.add(artist!!.name!!) }
val searchQuery = "${it.name} - ${artistsList.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim())
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
object : Callback<String>{
override fun onResponse(call: Call<String>, response: Response<String>) {
sharedViewModel?.uiScope?.launch {
val videoId = sortByBestMatch(
getYTTracks(response.body().toString()),
trackName = it.name.toString(),
trackArtists = artistsList,
trackDurationSec = (it.duration_ms/1000).toInt()
trackName = it.title,
trackArtists = it.artists,
trackDurationSec = it.durationSec
).keys.firstOrNull()
Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
if(videoId.isNullOrBlank()) {notFound++ ; updateStatusBar()}
else {//Found Youtube Video ID
val trackDetails = 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 outputFile: String =
Environment.getExternalStorageDirectory().toString() + File.separator +
defaultDir +
removeIllegalChars(type) + File.separator +
(if (subFolder == null) { "" }
else { removeIllegalChars(subFolder) + File.separator }
+ removeIllegalChars(it.name!!) + ".m4a")
+ removeIllegalChars(it.title) + ".m4a")
val downloadObject = DownloadObject(
trackDetails = trackDetails,
trackDetails = it,
ytVideoId = videoId,
outputFile = outputFile
)
@ -150,6 +141,13 @@ object SpotifyDownloadHelper {
}
}
private fun resetStatusBar() {
total = 0
processed = 0
notFound = 0
updateStatusBar()
}
private fun animateStatusBar() {
val anim: Animation = AlphaAnimation(0.3f, 0.9f)
anim.duration = 1500 //You can manage the blinking time with this parameter

View File

@ -24,7 +24,9 @@ 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.isOnline
import com.shabinder.spotiflyer.utils.removeIllegalChars
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
import com.shabinder.spotiflyer.utils.startService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -38,6 +40,10 @@ object YTDownloadHelper {
){
val downloadList = ArrayList<DownloadObject>()
tracks.forEach {
if(!isOnline()){
showNoConnectionAlert()
return
}
val outputFile: String =
Environment.getExternalStorageDirectory().toString() + File.separator +
defaultDir +

View File

@ -85,7 +85,7 @@ fun getYTTracks(response: String):List<YoutubeTrack>{
! Songs details are ALWAYS in the following order:
! 0 - Name
! 1 - Type (Song)
! 2 - Artist
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
! 3 - Album
! 4 - Duration (mm:ss)
!

View File

@ -18,6 +18,7 @@
package com.shabinder.spotiflyer.models
import android.os.Parcelable
import com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.android.parcel.Parcelize
import java.io.File

View File

@ -17,15 +17,7 @@
package com.shabinder.spotiflyer.models
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
data class YTTrack(
var id:String?,
var title:String?,
var duration:Int?,
var author:String?,
var viewCount:Long?,
var thumbnails:List<String?>?
):Parcelable
@Serializable
data class Optional<T>(val value: T?)

View File

@ -0,0 +1,24 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class Artist (
val popularity : Int,
val seokey : String,
val name : String,
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
import com.squareup.moshi.Json
data class CustomArtworks (
@Json(name = "40x40") val size_40p : String,
@Json(name = "80x80") val size_80p : String,
@Json(name = "110x110")val size_110p : String,
@Json(name = "175x175")val size_175p : String,
@Json(name = "480x480")val size_480p : String,
)

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class GaanaAlbum (
val tracks : List<Tracks>,
val count : Int,
val custom_artworks : CustomArtworks,
val release_year : Int,
val favorite_count : Int,
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class GaanaArtistDetails(
val artist : List<Artist>,
val count : Int,
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class GaanaArtistTracks(
val count : Int,
val tracks : List<Tracks>
)

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class GaanaPlaylist (
val tags : String,
val fromCache : Int,
val modified_on : String,
val count : Int,
val created_on : String,
val favorite_count : Int,
val tracks : List<Tracks>,
)

View File

@ -0,0 +1,22 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class GaanaSong(
val tracks : List<Tracks>
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class Genre (
val genre_id : Int,
val name : String
)

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
data class Tags (
val tag_id : Int,
val tag_name : String
)

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.gaana
import com.squareup.moshi.Json
data class Tracks (
val tags : List<Tags>,
val seokey : String,
val albumseokey : String,
val track_title : String,
val album_title : String,
val language : String,
@Json(name = "artwork_large") val artworkLink : String,
val artist : List<Artist>,
@Json(name = "gener") val genre : List<Genre>,
val lyrics_url : String,
val youtube_id : String,
val total_favourite_count : Int,
val release_date : String,
val play_ct : String,
val secondary_language : String,
)

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import com.squareup.moshi.Json

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models.spotify
enum class Source {
Spotify,
YouTube,
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import com.shabinder.spotiflyer.models.DownloadStatus
import kotlinx.android.parcel.Parcelize
@Parcelize
@ -39,5 +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):Parcelable
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
):Parcelable

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.models
package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

View File

@ -0,0 +1,101 @@
/*
* 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.networking
import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.gaana.*
import retrofit2.http.GET
import retrofit2.http.Query
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
interface GaanaInterface {
/*
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular_playlist" , "playlist_home_featured" ,"playlist_detail" ,"user_playlist" ,"topCharts"]
**/
@GET
suspend fun getGaanaPlaylist(
@Query("type") type: String = "playlist",
@Query("subtype") subtype: String = "playlist_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 2000
): Optional<GaanaPlaylist>
/*
* Api Request: http://api.gaana.com/?type=album&subtype=album_detail&seokey=kabir-singh&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "new_release" ,"featured_album" ,"similar_album" ,"all_albums", "album" ,"album_detail" ,"album_detail_info"]
**/
@GET
suspend fun getGaanaAlbum(
@Query("type") type: String = "album",
@Query("subtype") subtype: String = "album_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 2000
): Optional<GaanaAlbum>
/*
* Api Request: http://api.gaana.com/?type=song&subtype=song_detail&seokey=pachtaoge&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "hot_songs" ,"recommendation" ,"song_detail"]
**/
@GET
suspend fun getGaanaSong(
@Query("type") type: String = "song",
@Query("subtype") subtype: String = "song_detail",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
): Optional<GaanaSong>
/*
* Api Request: https://api.gaana.com/?type=artist&subtype=artist_details_info&seokey=neha-kakkar&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
**/
@GET
suspend fun getGaanaArtistDetails(
@Query("type") type: String = "artist",
@Query("subtype") subtype: String = "artist_details_info",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
): Optional<GaanaArtistDetails>
/*
* Api Request: http://api.gaana.com/?type=artist&subtype=artist_track_listing&seokey=neha-kakkar&limit=50&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
*
* subtype : ["most_popular" , "artist_list" ,"artist_track_listing" ,"artist_album" ,"similar_artist","artist_details" ,"artist_details_info"]
**/
@GET
suspend fun getGaanaArtistTracks(
@Query("type") type: String = "artist",
@Query("subtype") subtype: String = "artist_track_listing",
@Query("seokey") seokey: String,
@Query("token") token: String = gaana_token,
@Query("format") format: String = "JSON",
@Query("limit") limit: Int = 50
): Optional<GaanaArtistTracks>
}

View File

@ -15,58 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.utils
package com.shabinder.spotiflyer.networking
import com.shabinder.spotiflyer.models.*
import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.spotify.*
import retrofit2.http.*
/*
* 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/>.
*/
interface SpotifyService {
@GET("playlists/{playlist_id}")
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Playlist
suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Optional<Playlist>
@GET("playlists/{playlist_id}/tracks")
suspend fun getPlaylistTracks(
@Path("playlist_id") playlistId: String?,
@Query("offset") offset: Int = 0,
@Query("limit") limit: Int = 100
): PagingObjectPlaylistTrack
): Optional<PagingObjectPlaylistTrack>
@GET("tracks/{id}")
suspend fun getTrack(@Path("id") trackId: String?): Track
suspend fun getTrack(@Path("id") trackId: String?): Optional<Track>
@GET("episodes/{id}")
suspend fun getEpisode(@Path("id") episodeId: String?): Track
suspend fun getEpisode(@Path("id") episodeId: String?): Optional<Track>
@GET("shows/{id}")
suspend fun getShow(@Path("id") showId: String?): Track
suspend fun getShow(@Path("id") showId: String?): Optional<Track>
@GET("albums/{id}")
suspend fun getAlbum(@Path("id") albumId: String?): Album
suspend fun getAlbum(@Path("id") albumId: String?): Optional<Album>
}
interface SpotifyServiceTokenRequest{
@POST("api/token")
@FormUrlEncoded
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"):Token?
suspend fun getToken(@Field("grant_type") grant_type:String = "client_credentials"): Optional<Token>
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.utils
package com.shabinder.spotiflyer.networking
import com.beust.klaxon.JsonObject
import retrofit2.Call
@ -25,21 +25,12 @@ import retrofit2.http.POST
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
/*val body = """{
"context": {
"client": {
"clientName": "WEB_REMIX",
"clientVersion": "0.1"
}
},
"query": "songSearchQuery"
}"""*/
interface YoutubeMusicApi {
@Headers("Content-Type: application/json", "Referer: https://music.youtube.com/search")
@POST("search?alt=json&key=$apiKey")
fun getYoutubeMusicResponse(@Body text: JsonObject): Call<String>
fun getYoutubeMusicResponse(@Body text: String): Call<String>
}
fun makeJsonBody(query: String):JsonObject{

View File

@ -18,6 +18,7 @@
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
@ -27,23 +28,21 @@ 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.SpotifyDownloadHelper.downloadAllTracks
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.Track
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 com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.rotateAnim
import kotlinx.coroutines.launch
import java.io.File
class SpotifyTrackListAdapter(private val spotifyViewModel : SpotifyViewModel): ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(SpotifyTrackDiffCallback()) {
class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHolder>(SpotifyTrackDiffCallback()) {
var spotifyViewModel : SpotifyViewModel? = null
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)
@ -55,7 +54,7 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
val item = getItem(position)
if(itemCount ==1 || isAlbum){
holder.binding.imageUrl.visibility = View.GONE}else{
spotifyViewModel!!.uiScope.launch {
spotifyViewModel.uiScope.launch {
//Placeholder Set
bindImage(holder.binding.imageUrl, item.album?.images?.get(0)?.url, Source.Spotify)
}
@ -77,14 +76,35 @@ class SpotifyTrackListAdapter: ListAdapter<Track,SpotifyTrackListAdapter.ViewHol
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<Track>()
itemList.add(item)
downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList)
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!
}

View File

@ -20,19 +20,16 @@ package com.shabinder.spotiflyer.recyclerView
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 com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source
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.Provider
import com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.rotateAnim
import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch
class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): ListAdapter<TrackDetails,SpotifyTrackListAdapter.ViewHolder>(YouTubeTrackDiffCallback()) {
@ -74,7 +71,11 @@ class YoutubeTrackListAdapter(private val youtubeViewModel :YoutubeViewModel): L
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()
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
showMessage("Processing!")
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading

View File

@ -1,5 +1,5 @@
D/Retrofit: <--- HTTP 200 https://api.spotify.com/v1/me/top/artists (7170ms)
2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.Artist@4fae9ec, kaaes.spotify.webapi.android.models.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.Artist@ed6004a, kaaes.spotify.webapi.android.models.Artist@870dbbb, kaaes.spotify.webapi.android.models.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.Artist@aab431, kaaes.spotify.webapi.android.models.Artist@a7bd716, kaaes.spotify.webapi.android.models.Artist@3477897, kaaes.spotify.webapi.android.models.Artist@7f68a84]
2020-07-17 18:24:00.718 25414-25414/com.shabinder.musicforeveryone I/Network: [kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@4fae9ec, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aa3b1b5, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@ed6004a, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@870dbbb, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@8a2b8d8, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@aab431, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@a7bd716, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@3477897, kaaes.spotify.webapi.android.models.com.shabinder.spotiflyer.models.gaana.Artist@7f68a84]
I/Network: https://api.spotify.com/v1/artists/7vk5e3vY1uw9plTHJAMwjN

View File

@ -0,0 +1,54 @@
/*
* 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.ui.gaana
import android.content.BroadcastReceiver
import android.content.IntentFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import javax.inject.Inject
class GaanaFragment : Fragment() {
private lateinit var binding: TrackListFragmentBinding
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
private lateinit var viewModel: GaanaViewModel
@Inject lateinit var gaanaInterface: GaanaInterface
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.track_list_fragment, container, false)
viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java)
return binding.root
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.ui.gaana
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.database.DatabaseDAO
class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : ViewModel()

View File

@ -25,14 +25,18 @@ import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.isOnline
import com.shabinder.spotiflyer.utils.showMessage
import com.shabinder.spotiflyer.utils.showNoConnectionAlert
import com.shreyaspatil.easyupipayment.EasyUpiPayment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
@ -55,14 +59,20 @@ class MainFragment : Fragment() {
): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
initializeAll()
binding.btnSearch.setOnClickListener {
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
val link = binding.linkSearch.text.toString()
if (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) ){
findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link))
}else{Toast.makeText(context,"Link is Not Valid",Toast.LENGTH_SHORT).show()}
}else showMessage("Link is Not Valid",true)
}
handleIntent()
return binding.root
@ -97,10 +107,15 @@ class MainFragment : Fragment() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{
if(it != ""){
sharedViewModel.uiScope.launch(Dispatchers.IO) {
while (sharedViewModel.accessToken.value == "") {
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()

View File

@ -22,46 +22,43 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.SimpleItemAnimator
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.SpotifyFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.Track
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.utils.YoutubeMusicApi
import com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.loadAllImages
import com.shabinder.spotiflyer.utils.rotateAnim
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@Suppress("DEPRECATION")
@AndroidEntryPoint
class SpotifyFragment : Fragment() {
private lateinit var binding:SpotifyFragmentBinding
private lateinit var spotifyViewModel: SpotifyViewModel
private lateinit var binding:TrackListFragmentBinding
private lateinit var sharedViewModel: SharedViewModel
private lateinit var adapterSpotify:SpotifyTrackListAdapter
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
private lateinit var viewModel: SpotifyViewModel
private lateinit var adapter:SpotifyTrackListAdapter
private var intentFilter:IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
@ -71,8 +68,7 @@ class SpotifyFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater,R.layout.spotify_fragment,container,false)
adapterSpotify = SpotifyTrackListAdapter()
binding = DataBindingUtil.inflate(inflater,R.layout.track_list_fragment,container,false)
initializeAll()
initializeLiveDataObservers()
initializeBroadcast()
@ -88,34 +84,35 @@ class SpotifyFragment : Fragment() {
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
(activity as MainActivity).authenticateSpotify()
}
if(!isOnline()){//Device Offline
sharedViewModel.showAlertDialog(resources,requireContext())
}else if (type == "Error" || link == "Error") {//Incorrect Link
showToast("Please Check Your Link!")
if (type == "Error" || link == "Error") {//Incorrect Link
showMessage("Please Check Your Link!")
}else if(spotifyLink.contains("open.spotify",true)){//Link Validation!!
if(type == "episode" || type == "show"){//TODO Implementation
showToast("Implementing Soon, Stay Tuned!")
showMessage("Implementing Soon, Stay Tuned!")
}
else{
spotifyViewModel.spotifySearch(type,link)
if(type=="album")adapterSpotify.isAlbum = true
viewModel.spotifySearch(type,link)
if(type=="album")adapter.isAlbum = true
binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE
rotateAnim(binding.downloadingFab)
for (track in spotifyViewModel.trackList.value!!){
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading
adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track))
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
showToast("Processing!")
showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>()
spotifyViewModel.trackList.value?.forEach { urlList.add(it.album?.images?.get(0)?.url.toString()) }
viewModel.trackList.value?.forEach { urlList.add(it.album?.images?.get(0)?.url.toString()) }
//Appending Source
urlList.add("spotify")
loadAllImages(
@ -123,11 +120,29 @@ class SpotifyFragment : Fragment() {
urlList
)
}
spotifyViewModel.uiScope.launch {
SpotifyDownloadHelper.downloadAllTracks(
spotifyViewModel.folderType,
spotifyViewModel.subFolder,
spotifyViewModel.trackList.value!!,
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
)
}
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks(
viewModel.folderType,
viewModel.subFolder,
finalList ?: listOf(),
)
}
}
@ -136,43 +151,23 @@ class SpotifyFragment : Fragment() {
return binding.root
}
override fun onResume() {
super.onResume()
initializeBroadcast()
/**
* Basic Initialization
**/
private fun initializeAll() {
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
viewModel.spotifyService = it
})
adapter = SpotifyTrackListAdapter(viewModel)
DownloadHelper.youtubeMusicApi = youtubeMusicApi
DownloadHelper.sharedViewModel = sharedViewModel
DownloadHelper.statusBar = binding.statusBar
binding.trackList.adapter = adapter
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
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<TrackDetails?>("track")
trackDetails?.let {
val position: Int = spotifyViewModel.trackList.value?.map { it.name }?.indexOf(trackDetails.title) ?: -1
Log.i("Track","Download Completed Intent :$position")
if(position != -1) {
val track = spotifyViewModel.trackList.value?.get(position)
track?.let{
it.downloaded = DownloadStatus.Downloaded
spotifyViewModel.trackList.value?.set(position, it)
adapterSpotify.notifyItemChanged(position)
checkIfAllDownloaded()
}
}
}
}
}
}
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(updateUIReceiver)
}
/**
*Live Data Observers
@ -181,17 +176,17 @@ class SpotifyFragment : Fragment() {
/**
* CoverUrl Binding Observer!
**/
spotifyViewModel.coverUrl.observe(viewLifecycleOwner, {
viewModel.coverUrl.observe(viewLifecycleOwner, {
if(it!="Loading") bindImage(binding.coverImage,it, Source.Spotify)
})
/**
* TrackList Binding Observer!
**/
spotifyViewModel.trackList.observe(viewLifecycleOwner, {
viewModel.trackList.observe(viewLifecycleOwner, {
if (it.isNotEmpty()){
Log.i("SpotifyFragment","TrackList Updated")
adapterConfig(it)
adapter.submitList(it)
checkIfAllDownloaded()
}
})
@ -199,7 +194,7 @@ class SpotifyFragment : Fragment() {
/**
* Title Binding Observer!
**/
spotifyViewModel.title.observe(viewLifecycleOwner, {
viewModel.title.observe(viewLifecycleOwner, {
binding.titleView.text = it
})
@ -213,7 +208,7 @@ class SpotifyFragment : Fragment() {
}
private fun checkIfAllDownloaded() {
if(!spotifyViewModel.trackList.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{
@ -224,47 +219,41 @@ class SpotifyFragment : Fragment() {
}
}
}
private fun initializeBroadcast() {
intentFilter = IntentFilter()
intentFilter?.addAction("track_download_completed")
/**
* Basic Initialization
**/
private fun initializeAll() {
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer {
spotifyViewModel.spotifyService = it
})
SpotifyDownloadHelper.youtubeMusicApi = youtubeMusicApi
SpotifyDownloadHelper.sharedViewModel = sharedViewModel
SpotifyDownloadHelper.statusBar = binding.statusBar
binding.trackList.adapter = adapterSpotify
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = viewModel.trackList.value?.map { it.name }?.indexOf(trackDetails.title) ?: -1
Log.i("Track","Download Completed Intent :$position")
if(position != -1) {
val track = viewModel.trackList.value?.get(position)
track?.let{
it.downloaded = DownloadStatus.Downloaded
viewModel.trackList.value?.set(position, it)
adapter.notifyItemChanged(position)
checkIfAllDownloaded()
}
}
}
}
}
}
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
}
/**
* Configure Recycler View Adapter
**/
private fun adapterConfig(trackList: List<Track>){
adapterSpotify.spotifyViewModel = spotifyViewModel
adapterSpotify.submitList(trackList)
override fun onResume() {
super.onResume()
initializeBroadcast()
}
/**
* Util. Function to create toasts!
**/
private fun showToast(message:String){
Toast.makeText(context,message,Toast.LENGTH_SHORT).show()
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(updateUIReceiver)
}
/**
* Util. Function To Check Connection Status
**/
private fun isOnline(): Boolean {
val cm =
requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val netInfo = cm.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
}
}

View File

@ -23,8 +23,9 @@ 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.*
import com.shabinder.spotiflyer.utils.SpotifyService
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.spotify.*
import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.finalOutputDir
import kotlinx.coroutines.*
import java.io.File
@ -153,19 +154,19 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
private suspend fun getTrackDetails(trackLink:String): Track?{
Log.i("Requesting","https://api.spotify.com/v1/tracks/$trackLink")
return spotifyService?.getTrack(trackLink)
return spotifyService?.getTrack(trackLink)?.value
}
private suspend fun getAlbumDetails(albumLink:String): Album?{
Log.i("Requesting","https://api.spotify.com/v1/albums/$albumLink")
return spotifyService?.getAlbum(albumLink)
return spotifyService?.getAlbum(albumLink)?.value
}
private suspend fun getPlaylistDetails(link:String): Playlist?{
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link")
return spotifyService?.getPlaylist(link)
return spotifyService?.getPlaylist(link)?.value
}
private suspend fun getPlaylistTrackDetails(link:String,offset:Int = 0,limit:Int = 100): PagingObjectPlaylistTrack?{
Log.i("Requesting","https://api.spotify.com/v1/playlists/$link/tracks?offset=$offset&limit=$limit")
return spotifyService?.getPlaylistTracks(link, offset, limit)
return spotifyService?.getPlaylistTracks(link, offset, limit)?.value
}
override fun onCleared() {

View File

@ -26,22 +26,19 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.SharedViewModel
import com.shabinder.spotiflyer.databinding.YoutubeFragmentBinding
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter
import com.shabinder.spotiflyer.utils.bindImage
import com.shabinder.spotiflyer.utils.loadAllImages
import com.shabinder.spotiflyer.utils.rotateAnim
import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -50,24 +47,24 @@ import javax.inject.Inject
@AndroidEntryPoint
class YoutubeFragment : Fragment() {
private lateinit var binding:YoutubeFragmentBinding
private lateinit var youtubeViewModel: YoutubeViewModel
private lateinit var binding: TrackListFragmentBinding
private lateinit var viewModel: YoutubeViewModel
private lateinit var sharedViewModel: SharedViewModel
@Inject lateinit var ytDownloader: YoutubeDownloader
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
private val sampleDomain2 = "youtu.be"
private val sampleDomain1 = "youtube.com"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.youtube_fragment,container,false)
youtubeViewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java)
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(youtubeViewModel)
adapter = YoutubeTrackListAdapter(viewModel)
binding.trackList.adapter = adapter
initializeLiveDataObservers()
@ -84,7 +81,7 @@ class YoutubeFragment : Fragment() {
if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
youtubeViewModel.getYTPlaylist(playlistId,ytDownloader)
viewModel.getYTPlaylist(playlistId,ytDownloader)
}else{//Given Link is of a Video
var searchId = "error"
if(link.contains(sampleDomain1,true) ){
@ -94,29 +91,33 @@ class YoutubeFragment : Fragment() {
searchId = link.substringAfterLast("/","error")
}
if(searchId != "error") {
youtubeViewModel.getYTTrack(searchId,ytDownloader)
}else{showToast("Your Youtube Link is not of a Video!!")}
viewModel.getYTTrack(searchId,ytDownloader)
}else{showMessage("Your Youtube Link is not of a Video!!")}
}
/*
* Download All Tracks
* */
binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){
showNoConnectionAlert()
return@setOnClickListener
}
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.visibility = View.VISIBLE
rotateAnim(binding.downloadingFab)
for (track in youtubeViewModel.ytTrackList.value?: listOf()){
for (track in viewModel.ytTrackList.value?: listOf()){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading
adapter.notifyItemChanged(youtubeViewModel.ytTrackList.value!!.indexOf(track))
adapter.notifyItemChanged(viewModel.ytTrackList.value!!.indexOf(track))
}
}
showToast("Processing!")
showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>()
youtubeViewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
viewModel.ytTrackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast(".")}/hqdefault.jpg")}
//Appending Source
urlList.add("youtube")
@ -125,11 +126,11 @@ class YoutubeFragment : Fragment() {
urlList
)
}
youtubeViewModel.uiScope.launch {
viewModel.uiScope.launch {
YTDownloadHelper.downloadYTTracks(
type = youtubeViewModel.folderType,
subFolder = youtubeViewModel.subFolder,
tracks = youtubeViewModel.ytTrackList.value ?: listOf()
type = viewModel.folderType,
subFolder = viewModel.subFolder,
tracks = viewModel.ytTrackList.value ?: listOf()
)
}
}
@ -149,13 +150,13 @@ class YoutubeFragment : Fragment() {
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = youtubeViewModel.ytTrackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
val position: Int = viewModel.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)
val track = viewModel.ytTrackList.value?.get(position)
track?.let{
it.downloaded = DownloadStatus.Downloaded
youtubeViewModel.ytTrackList.value?.set(position, it)
viewModel.ytTrackList.value?.set(position, it)
adapter.notifyItemChanged(position)
checkIfAllDownloaded()
}
@ -173,7 +174,7 @@ class YoutubeFragment : Fragment() {
}
private fun checkIfAllDownloaded() {
if(!youtubeViewModel.ytTrackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
if(!viewModel.ytTrackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){
//All Tracks Downloaded
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
@ -188,38 +189,23 @@ class YoutubeFragment : Fragment() {
/**
* CoverUrl Binding Observer!
**/
youtubeViewModel.coverUrl.observe(viewLifecycleOwner, {
viewModel.coverUrl.observe(viewLifecycleOwner, {
if(it!="Loading") bindImage(binding.coverImage,it, Source.YouTube)
})
/**
* TrackList Binding Observer!
**/
youtubeViewModel.ytTrackList.observe(viewLifecycleOwner, {
adapterConfig(it)
viewModel.ytTrackList.observe(viewLifecycleOwner, {
adapter.submitList(it)
})
/**
* Title Binding Observer!
**/
youtubeViewModel.title.observe(viewLifecycleOwner, {
viewModel.title.observe(viewLifecycleOwner, {
binding.titleView.text = it
})
}
/**
* Configure Recycler View Adapter
**/
private fun adapterConfig(list:List<TrackDetails>){
adapter.submitList(list)
}
/**
* Util. Function to create toasts!
**/
private fun showToast(message:String){
Toast.makeText(context,message, Toast.LENGTH_SHORT).show()
}
}

View File

@ -24,17 +24,15 @@ import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.formats.Format
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.Source
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.models.spotify.Source
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 com.shabinder.spotiflyer.utils.showMessage
import kotlinx.coroutines.*
import java.io.File
@ -47,7 +45,6 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
* */
val ytTrackList = MutableLiveData<MutableList<TrackDetails>>()
val format = MutableLiveData<Format>()
private val loading = "Loading"
var title = MutableLiveData<String>().apply { value = "\"Loading!\"" }
var coverUrl = MutableLiveData<String>().apply { value = loading }
@ -108,7 +105,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
}
}
}catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){
showToast("An Error Occurred While Processing!")
showMessage("An Error Occurred While Processing!")
}
}
@ -124,16 +121,18 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: ""
Log.i("YT View Model",detail.toString())
ytTrackList.postValue(
listOf(TrackDetails(
listOf(
TrackDetails(
title = name,
artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0,
albumArt = File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + searchId + ".jpeg"
defaultDir +".Images/" + searchId + ".jpeg"
),
source = Source.YouTube
)).toMutableList()
)
).toMutableList()
)
title.postValue(
if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}
@ -152,7 +151,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
}
}
} catch (e:com.github.kiulian.downloader.YoutubeException){
showToast("An Error Occurred While Processing!")
showMessage("An Error Occurred While Processing!")
}
}
}

View File

@ -0,0 +1,66 @@
/*
* 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 okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return if (!isOnline()){
//No Internet Connection
showNoConnectionAlert()
//Lets Stop the Incoming Request
Response.Builder()
.code(NoInternetErrorCode) // code(200.300) = successful else = unsuccessful
.body("{}".toResponseBody(null)) // Whatever body
.protocol(Protocol.HTTP_2)
.message("No Internet Connection")
.request(chain.request())
.build()
}else {
val response = chain.proceed(chain.request())
val responseBody = response.body
val bodyString = responseBody?.string()
//Log.i("Network Request",bodyString)
//chain.proceed(chain.request())
//Log.i("Network Request","{\"unchecked\":${bodyString}}")
Response.Builder()
.code(response.code) // code(200.300) = successful else = unsuccessful
.body("{\"value\":${bodyString}}".toResponseBody(responseBody?.contentType())) // Whatever body
.protocol(response.protocol)
.message(response.message)
.request(chain.request())
.build()
}
}
/*
* Converts REQUEST's Body to String
* */
private fun RequestBody?.bodyToString(): String {
if (this == null) return ""
val buffer = okio.Buffer()
writeTo(buffer)
return buffer.readUtf8()
}
}

View File

@ -19,12 +19,14 @@ 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
import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecordDatabase
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shreyaspatil.easyupipayment.EasyUpiPayment
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
@ -37,12 +39,12 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import javax.inject.Singleton
@InstallIn(ApplicationComponent::class)
@Module
object Provider {
@ -66,7 +68,7 @@ object Provider {
@Provides
@Singleton
fun provideUpi():EasyUpiPayment {
return EasyUpiPayment.Builder(MainActivity.getInstance())
return EasyUpiPayment.Builder(activity)
.setPayeeVpa("technoshab@paytm")
.setPayeeName("Shabinder Singh")
.setTransactionId("UNIQUE_TRANSACTION_ID")
@ -86,39 +88,59 @@ object Provider {
@Provides
@Singleton
fun getSpotifyTokenInterface():SpotifyServiceTokenRequest{
fun getSpotifyTokenInterface(moshi: Moshi): SpotifyServiceTokenRequest {
val httpClient2: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient2.addInterceptor(Interceptor { chain ->
.addInterceptor(Interceptor { chain ->
val request: Request =
chain.request().newBuilder().addHeader(
chain.request().newBuilder()
.addHeader(
"Authorization",
"Basic ${android.util.Base64.encodeToString("${App.clientId}:${App.clientSecret}".toByteArray(),android.util.Base64.NO_WRAP)}"
"Basic ${
android.util.Base64.encodeToString(
"${App.clientId}:${App.clientSecret}".toByteArray(),
android.util.Base64.NO_WRAP
)
}"
).build()
chain.proceed(request)
})
}).addInterceptor(NetworkInterceptor())
val retrofit = Retrofit.Builder()
.baseUrl("https://accounts.spotify.com/")
.client(httpClient2.build())
.addConverterFactory(MoshiConverterFactory.create(getMoshi()))
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(SpotifyServiceTokenRequest::class.java)
}
@Provides
@Singleton
fun getYoutubeMusicApi():YoutubeMusicApi{
fun okHttpClient():OkHttpClient{
return OkHttpClient.Builder()
.addInterceptor(NetworkInterceptor())
.build()
}
@Provides
@Singleton
fun getGaanaInterface(moshi: Moshi,okHttpClient: OkHttpClient):GaanaInterface{
val retrofit = Retrofit.Builder()
.baseUrl("http://api.gaana.com/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
return retrofit.create(GaanaInterface::class.java)
}
@Provides
@Singleton
fun getYoutubeMusicApi(moshi: Moshi): YoutubeMusicApi {
val retrofit = Retrofit.Builder()
.baseUrl("https://music.youtube.com/youtubei/v1/")
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
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()
}
}

View File

@ -19,6 +19,9 @@ package com.shabinder.spotiflyer.utils
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View
@ -33,9 +36,12 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.Source
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.worker.ForegroundService
import kotlinx.coroutines.CoroutineScope
@ -64,6 +70,48 @@ fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,e
+ itemName?.let { removeIllegalChars(it) + extension})
}
/**
* Util. Function To Check Connection Status
**/
@Suppress("DEPRECATION")
fun isOnline(): Boolean {
var result = false
val connectivityManager =
activity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
}
} else {
val netInfo =
(activity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
result = netInfo != null && netInfo.isConnected
}
}
return result
}
fun showMessage(message: String, long: Boolean = false){
CoroutineScope(Dispatchers.Main).launch{
Snackbar.make(
activity.snackBarAnchor,
message,
if (long) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT
).also { snackbar ->
snackbar.setAction("Ok") {
snackbar.dismiss()
}
}.show()
}
}
fun rotateAnim(view: View){
val rotate = RotateAnimation(
0F, 360F,
@ -76,6 +124,18 @@ fun rotateAnim(view: View){
view.animation = rotate
}
fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch {
activity.apply {
MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme)
.setTitle(resources.getString(R.string.title))
.setMessage(resources.getString(R.string.supporting_text))
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
// Respond to neutral button press
}.show()
}
}
}
fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

View File

@ -458,7 +458,7 @@ class ForegroundService : Service(){
}
/**
*Modifying Mp3 Tags with MetaData!
*Modifying Mp3 com.shabinder.spotiflyer.models.gaana.Tags with MetaData!
**/
private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File {
val id3v1Tag = ID3v1Tag().apply {

View File

@ -23,19 +23,15 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainActivity"
android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="match_parent">
<TextView
android:id="@+id/message"
<View
android:id="@+id/snackBarPosition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/text_background_accented"
android:padding="5dp"
android:visibility="gone"
android:paddingTop="6dp"
android:text="Authentication Needed"
android:textColor="@color/colorPrimary"
android:textSize="10dp"
android:layout_marginBottom="16dp"
android:background="@drawable/transparent"
android:visibility="invisible"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -47,7 +43,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/message"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -21,6 +21,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:background="@color/black"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@ -22,6 +22,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="match_parent"
android:layout_marginTop="25dp"
android:fitsSystemWindows="true"
@ -74,10 +75,10 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topLayout"
android:layout_width="match_parent"
android:background="@color/black"
android:foreground="@drawable/gradient"
android:layout_height="match_parent">
<ImageView
android:id="@+id/cover_image"
android:layout_width="0dp"

View File

@ -23,7 +23,7 @@
<data>
<variable
name="track"
type="com.shabinder.spotiflyer.models.Track" />
type="com.shabinder.spotiflyer.models.spotify.Track" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View File

@ -1,152 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 Shabinder Singh
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="25dp"
android:fitsSystemWindows="true">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_download_all"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:background="@drawable/btn_design"
android:drawableEnd="@drawable/ic_arrow_slim"
android:drawablePadding="4dp"
android:drawableTint="@color/black"
android:padding="12dp"
android:text="Download All |"
android:textColor="@color/black"
android:textSize="16sp"
android:visibility="visible"
app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/downloading_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="@color/black"
android:scaleType="fitCenter"
android:visibility="gone"
app:borderWidth="0dp"
app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center"
app:maxImageSize="38dp"
app:rippleColor="@color/colorPrimaryDark"
app:srcCompat="@drawable/ic_refresh"
app:tint="@null" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="230dp">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="#F2C102B7"
app:layout_scrollFlags="scroll|enterAlways|enterAlwaysCollapsed"
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
app:toolbarId="@+id/toolbar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/topLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="@drawable/gradient"
>
<ImageView
android:id="@+id/cover_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="23dp"
android:layout_marginBottom="3dp"
android:contentDescription="Album Cover"
android:padding="15dp"
android:src="@drawable/spotify_download"
android:visibility="visible"
app:layout_collapseMode="parallax"
app:layout_constraintBottom_toTopOf="@id/title_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/statusBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/text_background_accented"
android:fontFamily="@font/raleway_semibold"
android:foreground="@drawable/rounded_gradient"
android:gravity="center"
android:paddingLeft="12dp"
android:paddingTop="1dp"
android:paddingRight="12dp"
android:paddingBottom="1dp"
android:text="Total: 100 Processed: 50"
android:textAlignment="center"
android:textColor="@color/grey"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/cover_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/title_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginBottom="20dp"
android:background="#00000000"
android:fontFamily="@font/raleway_semibold"
android:gravity="end"
android:text='"SpotiFlyer"'
android:textAlignment="viewEnd"
android:textColor="#9AB3FF"
android:textSize="28sp"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="26dp"
android:visibility="visible"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View File

@ -26,7 +26,7 @@
android:id="@+id/spotifyFragment"
android:name="com.shabinder.spotiflyer.ui.spotify.SpotifyFragment"
android:label="main_fragment"
tools:layout="@layout/spotify_fragment" >
tools:layout="@layout/track_list_fragment" >
<argument
android:name="link"
app:argType="string" />
@ -56,7 +56,7 @@
android:id="@+id/youtubeFragment"
android:name="com.shabinder.spotiflyer.ui.youtube.YoutubeFragment"
android:label="YoutubeFragment"
tools:layout="@layout/youtube_fragment">
tools:layout="@layout/track_list_fragment">
<argument
android:name="link"
app:argType="string" />

View File

@ -21,7 +21,6 @@
<!-- Customize your theme here. -->
<item name="colorPrimaryDark">#000000</item>
<item name="colorPrimary">#FC5C7D</item>
<item name="android:background">#000000</item>
<item name="android:textColor">#FFFFFF</item>
<item name="colorAccent">#6A82FB</item>
<item name="android:outlineAmbientShadowColor" tools:targetApi="p">#A9B200FF</item>
@ -29,7 +28,7 @@
<!-- Text Appearances !-->
<!-- use our brand's custom TextAppearance4 !-->
<item name="textAppearanceHeadline4">@style/TextAppearance.AppTheme.Headline4</item>
<!-- use default Body2 text apperance !-->
<!-- use default Body2 text appearance !-->
<item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item>
</style>
@ -38,18 +37,22 @@
<item name="shapeAppearanceMediumComponent">@style/CutShapeAppearance</item>
<item name="buttonBarPositiveButtonStyle">@style/Alert.Button.Positive</item>
<item name="buttonBarNeutralButtonStyle">@style/Alert.Button.Neutral</item>
<item name="android:textSize">22sp</item>
<item name="fontFamily">@font/amita</item>
</style>
<style name="CutShapeAppearance" parent="ShapeAppearance.MaterialComponents.MediumComponent">
<item name="background">@color/white</item>
<item name="cornerFamily">rounded</item>
<item name="cornerSize">20dp</item>
<item name="cornerSize">8dp</item>
</style>
<style name="Alert.Button.Positive" parent="Widget.MaterialComponents.Button.TextButton">
<item name="backgroundTint">@color/colorPrimary</item>
<item name="rippleColor">@color/colorPrimaryDark</item>
<item name="rippleColor">@color/cardview_dark_background</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textSize">14sp</item>
<item name="android:layout_marginEnd">4dp</item>
<item name="android:layout_marginBottom">2dp</item>
<item name="android:textAllCaps">false</item>
</style>

View File

@ -33,7 +33,7 @@ buildscript {
//safe-Args
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// 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
}