diff --git a/.idea/dictionaries/shabinder.xml b/.idea/dictionaries/shabinder.xml index 7218236f..82062cc1 100755 --- a/.idea/dictionaries/shabinder.xml +++ b/.idea/dictionaries/shabinder.xml @@ -1,14 +1,22 @@ + albumseokey + amita + cardview + cherrypick downloadrecord emoji ffmpeg flyer + gaana + gener + hqdefault insta instagram jetbrains kotlinx + linkedin mainfragment maxresdefault moshi @@ -17,14 +25,17 @@ musicplaceholder raleway semibold + seokey shabinder singh + snackbar spoti spotiflyer spotify spotifydownloader spotifyler thru + weyfdnx youtu diff --git a/app/build.gradle b/app/build.gradle index d390587f..86a31347 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,22 +21,23 @@ 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 buildToolsVersion "30.0.2" buildFeatures{ - dataBinding = true + //dataBinding = true + viewBinding = true } defaultConfig { applicationId 'com.shabinder.spotiflyer' minSdkVersion 22 targetSdkVersion 29 - versionCode 7 - versionName "1.5.1" + versionCode 8 + versionName "1.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } packagingOptions { @@ -90,8 +91,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" @@ -115,10 +118,13 @@ dependencies { implementation 'com.squareup.moshi:moshi:1.11.0' 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.beust:klaxon:5.4' + implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0' - implementation 'com.github.sealedtx:java-youtube-downloader:2.4.2' + implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4' implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5" implementation 'com.github.javiersantos:AppUpdater:2.7' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..503cf7ce 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 diff --git a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index b4a1272e..74c0f886 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -21,24 +21,24 @@ import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.net.Uri import android.os.Build import android.os.Bundle import android.os.PowerManager import android.provider.Settings import android.util.Log +import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavController +import androidx.navigation.findNavController import com.github.javiersantos.appupdater.AppUpdater import com.github.javiersantos.appupdater.enums.UpdateFrom import com.shabinder.spotiflyer.databinding.MainActivityBinding -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper -import com.shabinder.spotiflyer.utils.SpotifyService -import com.shabinder.spotiflyer.utils.SpotifyServiceTokenRequest -import com.shabinder.spotiflyer.utils.createDirectories +import com.shabinder.spotiflyer.networking.SpotifyService +import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest +import com.shabinder.spotiflyer.utils.* import com.squareup.moshi.Moshi import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -49,49 +49,48 @@ import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import javax.inject.Inject +/* +* This is App's God Activity +* */ @Suppress("DEPRECATION") @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 + private lateinit var navController: NavController @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) - sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) //Enabling Dark Mode AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - sharedPref = this.getPreferences(Context.MODE_PRIVATE) + binding = MainActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) + navController = findNavController(R.id.navHostFragment) + snackBarAnchor = binding.snackBarPosition - //starting Notification and Downloader Service! - SpotifyDownloadHelper.startService(this) - - if(sharedViewModel.spotifyService.value == null){ - authenticateSpotify() - }else{ - implementSpotifyService(sharedViewModel.accessToken.value!!) - } + authenticateSpotify() 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) handleIntentFromExternalActivity() } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Log.i("NEW INTENT", "Received") + //Return to MainFragment For Further Processing of this Intent + navController.popBackStack(R.id.mainFragment,false) handleIntentFromExternalActivity(intent) } @@ -102,9 +101,10 @@ class MainActivity : AppCompatActivity(){ this.getSystemService(Context.POWER_SERVICE) as PowerManager val isIgnoringBatteryOptimizations = pm.isIgnoringBatteryOptimizations(packageName) if (!isIgnoringBatteryOptimizations) { - val intent = Intent() - intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - intent.data = Uri.parse("package:$packageName") + val intent = Intent().apply{ + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:$packageName") + } startActivityForResult(intent, 1233) } } @@ -139,13 +139,13 @@ class MainActivity : AppCompatActivity(){ "Bearer $token" ).build() chain.proceed(request) - }) + }).addInterceptor(NetworkInterceptor()) val retrofit = Retrofit.Builder() - .baseUrl("https://api.spotify.com/v1/") - .client(httpClient.build()) - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .build() + .baseUrl("https://api.spotify.com/v1/") + .client(httpClient.build()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() spotifyService = retrofit.create(SpotifyService::class.java) sharedViewModel.spotifyService.value = spotifyService @@ -154,16 +154,13 @@ 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 { + showMessage("Success: Spotify Token Acquired",isSuccess = true) + implementSpotifyService(it.access_token) } + Log.i("Spotify Token", token.value.toString()) } } @@ -188,25 +185,14 @@ 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 .setUpdateFrom(UpdateFrom.XML) .setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml") .setCancelable(false) + .setButtonDoNotShowAgain("Remind Later") + .setButtonDoNotShowAgainClickListener { dialog, _ -> dialog.dismiss() } .setButtonUpdateClickListener { _, _ -> val uri: Uri = Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases") @@ -220,11 +206,10 @@ class MainActivity : AppCompatActivity(){ } companion object{ - private var instance = MainActivity() - fun getInstance():MainActivity{ - return instance - } + private lateinit var instance: MainActivity + fun getInstance():MainActivity = instance } + init { instance = this } diff --git a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt index 3705a905..7d8eb67f 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/SharedViewModel.kt @@ -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().apply { value = "" } + var intentString = MutableLiveData() var spotifyService = MutableLiveData() - var accessToken = MutableLiveData().apply { value = "" } - var isConnected = MutableLiveData().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 - } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt new file mode 100755 index 00000000..33c8973f --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/DownloadHelper.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.downloadHelper + +import android.annotation.SuppressLint +import android.os.Environment +import android.os.Handler +import android.util.Log +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import android.widget.TextView +import android.widget.Toast +import com.shabinder.spotiflyer.SharedViewModel +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.defaultDir +import com.shabinder.spotiflyer.utils.Provider.mainActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File + +object DownloadHelper { + + var statusBar:TextView? = null + var youtubeMusicApi: YoutubeMusicApi? = null + var sharedViewModel: SharedViewModel? = null + + private var total = 0 + private var processed = 0 + var notFound = 0 + + /** + * Function To Download All Tracks Available in a List + **/ + suspend fun downloadAllTracks( + type:String, + subFolder: String?, + trackList: List) { + resetStatusBar()// For New Download Request's Status + val downloadList = ArrayList() + 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 + Handler().postDelayed({ + //Delay is Added ,if a request is in processing it may finish + Log.i("Spotify Helper","Download Request Sent") + sharedViewModel?.uiScope?.launch (Dispatchers.Main){ + showMessage("Download Started, Now You can leave the App!") + } + startService(mainActivity,downloadList) + },3000) + } + }else{ + val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" + val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString() + youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue( + object : Callback{ + override fun onResponse(call: Call, response: Response) { + sharedViewModel?.uiScope?.launch { + val videoId = sortByBestMatch( + getYTTracks(response.body().toString()), + 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 outputFile: String = + Environment.getExternalStorageDirectory().toString() + File.separator + + defaultDir + + removeIllegalChars(type) + File.separator + + (if (subFolder == null) { "" } + else { removeIllegalChars(subFolder) + File.separator } + + removeIllegalChars(it.title) + ".m4a") + + val downloadObject = DownloadObject( + trackDetails = it, + ytVideoId = videoId, + outputFile = outputFile + ) + processed++ + sharedViewModel?.uiScope?.launch(Dispatchers.Main) { + updateStatusBar() + } + downloadList.add(downloadObject) + if(index == (trackList.size-1)){//LastElement + Handler().postDelayed({ + //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(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show() + } + startService(mainActivity,downloadList) + },5000) + } + } + } + } + override fun onFailure(call: Call, t: Throwable) { + if(t.message.toString().contains("Failed to connect")) showMessage("Failed, Check Your Internet Connection!") + Log.i("YT API Req. Fail",t.message.toString()) + } + } + ) + } + updateStatusBar() + } + animateStatusBar() + } + } + + 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 + anim.startOffset = 20 + anim.repeatMode = Animation.REVERSE + anim.repeatCount = Animation.INFINITE + statusBar?.animation = anim + } + + @SuppressLint("SetTextI18n") + fun updateStatusBar() { + statusBar!!.visibility = View.VISIBLE + statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/SpotifyDownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/SpotifyDownloadHelper.kt deleted file mode 100755 index a68427d6..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/SpotifyDownloadHelper.kt +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2020 Shabinder Singh - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.downloadHelper - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Environment -import android.os.Handler -import android.util.Log -import android.view.View -import android.view.animation.AlphaAnimation -import android.view.animation.Animation -import android.webkit.WebView -import android.webkit.WebViewClient -import android.widget.TextView -import androidx.core.content.ContextCompat -import com.github.kiulian.downloader.YoutubeDownloader -import com.github.kiulian.downloader.model.formats.Format -import com.github.kiulian.downloader.model.quality.AudioQuality -import com.shabinder.spotiflyer.models.DownloadObject -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel -import com.shabinder.spotiflyer.utils.getEmojiByUnicode -import com.shabinder.spotiflyer.worker.ForegroundService -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File - -object SpotifyDownloadHelper { - var webView:WebView? = null - var context : Context? = null - var statusBar:TextView? = null - val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator - var spotifyViewModel: SpotifyViewModel? = null - private var isBrowserLoading = false - private var total = 0 - private var Processed = 0 - private var notFound = 0 - private var listProcessed:Boolean = false - var youtubeList = mutableListOf() - - /** - * Function To Download All Tracks Available in a List - **/ - suspend fun downloadAllTracks( - type:String, - subFolder: String?, - trackList: List, ytDownloader: YoutubeDownloader?) { - withContext(Dispatchers.Main){ - total += trackList.size // Adding New Download List Count to StatusBar - trackList.forEach { - if(it.downloaded == "Downloaded"){//Download Already Present!! - Processed++ - }else{ - if(isBrowserLoading){//WebView Busy!! - if (listProcessed){//Previous List request progress check - getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it) - listProcessed = false//Notifying A list Processing Started - }else{//Adding Requests to a Queue - youtubeList.add(YoutubeRequest(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it)) - } - }else{ - getYTLink(type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it) - } - } - updateStatusBar() - } - animateStatusBar() - } - } - - - - //TODO CleanUp here and there!! - @SuppressLint("SetJavaScriptEnabled") - suspend fun getYTLink(type:String, - subFolder:String?, - ytDownloader: YoutubeDownloader?, - searchQuery: String, - track: Track){ - isBrowserLoading = true // Notify Web View Started Loading - val searchText = searchQuery.replace("\\s".toRegex(), "+") - val url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=$searchText" - Log.i("DH YT LINK ",url) - applyWebViewSettings(webView!!) - withContext(Dispatchers.Main){ - webView!!.loadUrl(url) - webView!!.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - view?.evaluateJavascript( - "document.getElementsByClassName(\"yt-simple-endpoint style-scope ytd-video-renderer\")[0].href" - ) { value -> - Log.i("YT-id Link", value.toString().replace("\"", "")) - val id = value!!.substringAfterLast("=", "error").replace("\"", "") - Log.i("YT-ID", id) - if (id != "error") {//Link extracting error - Processed++ - downloadFile(subFolder, type, track, ytDownloader, id) - }else notFound++ - updateStatusBar() - if (youtubeList.isNotEmpty()) { - val request = youtubeList[0] - spotifyViewModel!!.uiScope.launch { - getYTLink( - request.type, - request.subFolder, - request.ytDownloader, - request.searchQuery, - request.track - ) - } - youtubeList.remove(request) - if (youtubeList.size == 0) {//list processing completed , webView is free again! - isBrowserLoading = false - listProcessed = true - } - } else {//YT List Empty....Maybe it was one Single Download - Handler().postDelayed({//Delay of 1.5 sec - if (youtubeList.isEmpty()) {//Lets Make It sure , There are No more Downloads In Queue..... - isBrowserLoading = false - listProcessed = true - } - }, 1500) - } - } - } - } - } - } - - private fun updateStatusBar() { - statusBar!!.visibility = View.VISIBLE - statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $Processed ${getEmojiByUnicode(0x274C)}: $notFound" - } - - - fun downloadFile(subFolder: String?, type: String, track:Track, ytDownloader: YoutubeDownloader?, id: String) { - spotifyViewModel!!.uiScope.launch { - withContext(Dispatchers.IO) { - try { - val video = ytDownloader?.getVideo(id) - val detail = video?.details() - val format: Format? = try { - video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format - } catch (e: java.lang.IndexOutOfBoundsException) { - try { - video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format - } catch (e: java.lang.IndexOutOfBoundsException) { - try { - video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format - } catch (e: java.lang.IndexOutOfBoundsException) { - Log.i("YTDownloader", e.toString()) - null - } - } - } - format?.let { - val url: String = format.url() - Log.i("DHelper Link Found", url) - val outputFile: String = - Environment.getExternalStorageDirectory().toString() + File.separator + - defaultDir + removeIllegalChars(type) + File.separator + (if (subFolder == null) { - "" - } else { - removeIllegalChars(subFolder) + File.separator - } + removeIllegalChars(track.name!!) + ".m4a") - - val downloadObject = DownloadObject( - track = track, - url = url, - outputDir = outputFile - ) - Log.i("DH", outputFile) - startService(context!!, downloadObject) - } - }catch (e: com.github.kiulian.downloader.YoutubeException){ - Log.i("DH", "Error- Maybe Network") - } - } - } - } - - - fun startService(context:Context,obj:DownloadObject? = null ) { - val serviceIntent = Intent(context, ForegroundService::class.java) - serviceIntent.putExtra("object",obj) - ContextCompat.startForegroundService(context, serviceIntent) - } - - /** - * Removing Illegal Chars from File Name - * **/ - fun removeIllegalChars(fileName: String): String? { - val illegalCharArray = charArrayOf( - '/', - '\n', - '\r', - '\t', - '\u0000', - '\u000C', - '`', - '?', - '*', - '\\', - '<', - '>', - '|', - '\"', - '.', - '-', - '\'' - ) - - var name = fileName - for (c in illegalCharArray) { - name = fileName.replace(c, '_') - } - name = name.replace("\\s".toRegex(), "_") - name = name.replace("\\)".toRegex(), "") - name = name.replace("\\(".toRegex(), "") - name = name.replace("\\[".toRegex(), "") - name = name.replace("]".toRegex(), "") - name = name.replace("\\.".toRegex(), "") - name = name.replace("\"".toRegex(), "") - name = name.replace("\'".toRegex(), "") - name = name.replace(":".toRegex(), "") - name = name.replace("\\|".toRegex(), "") - return name - } - - private fun animateStatusBar() { - val anim: Animation = AlphaAnimation(0.0f, 0.9f) - anim.duration = 650 //You can manage the blinking time with this parameter - anim.startOffset = 20 - anim.repeatMode = Animation.REVERSE - anim.repeatCount = Animation.INFINITE - statusBar?.animation = anim - } - @SuppressLint("SetJavaScriptEnabled") - fun applyWebViewSettings(webView: WebView) { - val desktopUserAgent = - "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0" - val mobileUserAgent = - "Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30" - - //Choose Mobile/Desktop client. - webView.settings.userAgentString = desktopUserAgent - webView.settings.loadWithOverviewMode = true - webView.settings.builtInZoomControls = true - webView.settings.setSupportZoom(true) - webView.isScrollbarFadingEnabled = false - webView.scrollBarStyle = WebView.SCROLLBARS_OUTSIDE_OVERLAY - webView.settings.displayZoomControls = false - webView.settings.useWideViewPort = true - webView.settings.javaScriptEnabled = true - webView.settings.loadsImagesAutomatically = false - webView.settings.blockNetworkImage = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - webView.settings.safeBrowsingEnabled = true - } - } -} -data class YoutubeRequest( - val type:String, - val subFolder:String?, - val ytDownloader: YoutubeDownloader?, - val searchQuery: String, - val track: Track, - val index: Int? = null -) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt index 92856c54..2e6637fa 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YTDownloadHelper.kt @@ -17,49 +17,54 @@ package com.shabinder.spotiflyer.downloadHelper -import android.content.Context -import android.content.Intent import android.os.Environment import android.util.Log -import android.view.View -import android.widget.TextView -import androidx.core.content.ContextCompat -import com.github.kiulian.downloader.model.formats.Format +import android.widget.Toast import com.shabinder.spotiflyer.models.DownloadObject -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.worker.ForegroundService +import com.shabinder.spotiflyer.models.TrackDetails +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 +import com.shabinder.spotiflyer.utils.startService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File object YTDownloadHelper { - var context : Context? = null - var statusBar: TextView? = null - - fun downloadFile(subFolder: String?, type: String,ytTrack: Track,format: Format?) { - format?.let { - val url:String = format.url() -// Log.i("DHelper Link Found", url) - val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator + - SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} + SpotifyDownloadHelper.removeIllegalChars( - ytTrack.name!! - ) +".m4a") + suspend fun downloadYTTracks( + type:String, + subFolder: String?, + tracks:List, + ){ + val downloadList = ArrayList() + tracks.forEach { + if(!isOnline()){ + showNoConnectionAlert() + return + } + val outputFile: String = + Environment.getExternalStorageDirectory().toString() + File.separator + + defaultDir + + removeIllegalChars(type) + File.separator + + (if (subFolder == null) { "" } + else { removeIllegalChars(subFolder) + File.separator } + + removeIllegalChars(it.title) + ".m4a") val downloadObject = DownloadObject( - track = ytTrack, - url = url, - outputDir = outputFile + trackDetails = it, + ytVideoId = it.albumArt.absolutePath.substringAfterLast("/") + .substringBeforeLast("."), + outputFile = outputFile ) - Log.i("DH",outputFile) - startService(context!!, downloadObject) - statusBar?.visibility= View.VISIBLE + + downloadList.add(downloadObject) + } + Log.i("YT Downloader Helper","Download Request Sent") + withContext(Dispatchers.Main){ + Toast.makeText(mainActivity,"Download Started, Now You can leave the App!", Toast.LENGTH_SHORT).show() + startService(mainActivity,downloadList) } } - - - - private fun startService(context:Context, obj: DownloadObject? = null ) { - val serviceIntent = Intent(context, ForegroundService::class.java) - serviceIntent.putExtra("object",obj) - ContextCompat.startForegroundService(context, serviceIntent) - } - } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt new file mode 100644 index 00000000..2ffe02ea --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/downloadHelper/YoutubeProvider.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.downloadHelper + +import android.annotation.SuppressLint +import android.util.Log +import com.beust.klaxon.JsonArray +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import com.shabinder.spotiflyer.models.YoutubeTrack +import me.xdrop.fuzzywuzzy.FuzzySearch +import kotlin.math.absoluteValue + +/* +* Thanks To https://github.com/spotDL/spotify-downloader +* */ +fun getYTTracks(response: String):List{ + val youtubeTracks = mutableListOf() + + val stringBuilder: StringBuilder = StringBuilder(response) + val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject + val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array("contents") + val resultBlocks = mutableListOf>() + if (contentBlocks != null) { + for (cBlock in contentBlocks){ + /** + *Ignore user-suggestion + *The 'itemSectionRenderer' field is for user notices (stuff like - 'showing + *results for xyz, search for abc instead') we have no use for them, the for + *loop below if throw a keyError if we don't ignore them + */ + if(cBlock.containsKey("itemSectionRenderer")){ + continue + } + + for(contents in cBlock.obj("musicShelfRenderer")?.array("contents") ?: listOf()){ + /** + * apparently content Blocks without an 'overlay' field don't have linkBlocks + * I have no clue what they are and why there even exist + * + if(!contents.containsKey("overlay")){ + println(contents) + continue + TODO check and correct + }*/ + + val result = contents.obj("musicResponsiveListItemRenderer") + ?.array("flexColumns") + + //Add the linkBlock + val linkBlock = contents.obj("musicResponsiveListItemRenderer") + ?.obj("overlay") + ?.obj("musicItemThumbnailOverlayRenderer") + ?.obj("content") + ?.obj("musicPlayButtonRenderer") + ?.obj("playNavigationEndpoint") + + // detailsBlock is always a list, so we just append the linkBlock to it + // instead of carrying along all the other junk from "musicResponsiveListItemRenderer" + linkBlock?.let { result?.add(it) } + result?.let { resultBlocks.add(it) } + } + } + + /* We only need results that are Songs or Videos, so we filter out the rest, since + ! Songs and Videos are supplied with different details, extracting all details from + ! both is just carrying on redundant data, so we also have to selectively extract + ! relevant details. What you need to know to understand how we do that here: + ! + ! Songs details are ALWAYS in the following order: + ! 0 - Name + ! 1 - Type (Song) + ! 2 - com.shabinder.spotiflyer.models.gaana.Artist + ! 3 - Album + ! 4 - Duration (mm:ss) + ! + ! Video details are ALWAYS in the following order: + ! 0 - Name + ! 1 - Type (Video) + ! 2 - Channel + ! 3 - Viewers + ! 4 - Duration (hh:mm:ss) + ! + ! We blindly gather all the details we get our hands on, then + ! cherrypick the details we need based on their index numbers, + ! we do so only if their Type is 'Song' or 'Video + */ + + for(result in resultBlocks){ + + // Blindly gather available details + val availableDetails = mutableListOf() + + /* + Filter Out dummies here itself + ! 'musicResponsiveListItemFlexColumnRenderer' should have more that one + ! sub-block, if not its a dummy, why does the YTM response contain dummies? + ! I have no clue. We skip these. + + ! Remember that we appended the linkBlock to result, treating that like the + ! other constituents of a result block will lead to errors, hence the 'in + ! result[:-1] ,i.e., skip last element in array ' + */ + for(detail in result.subList(0,result.size-1)){ + if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue + + // if not a dummy, collect All Variables + detail.obj("musicResponsiveListItemFlexColumnRenderer") + ?.obj("text") + ?.array("runs")?.get(0)?.get("text")?.let { + availableDetails.add( + it.toString() + ) + } + } + Log.i("Text Api",availableDetails.toString()) + /* + ! Filter Out non-Song/Video results and incomplete results here itself + ! From what we know about detail order, note that [1] - indicate result type + */ + if ( availableDetails.size == 5 && availableDetails[1] in listOf("Song","Video") ){ + + // skip if result is in hours instead of minutes (no song is that long) + if(availableDetails[4].split(':').size != 2) continue //Has Been Giving Issues + + /* + ! grab Video ID + ! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...] + ! so hardcoding the dict keys for data look up is an ardours process, since + ! the sub-block pattern is fixed even though the key isn't, we just + ! reference the dict keys by index + */ + + val videoId:String = result.last().obj("watchEndpoint")?.get("videoId") as String + val ytTrack = YoutubeTrack( + name = availableDetails[0], + type = availableDetails[1], + artist = availableDetails[2], + duration = availableDetails[4], + videoId = videoId + ) + youtubeTracks.add(ytTrack) + } + } + } + + return youtubeTracks +} + +@SuppressLint("DefaultLocale") +fun sortByBestMatch(ytTracks:List, + trackName:String, + trackArtists:List, + trackDurationSec:Int, + ):Map{ + /* + * "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value + **/ + val linksWithMatchValue = mutableMapOf() + + for (result in ytTracks){ + + // LoweCasing Name to match Properly + // most song results on youtube go by $artist - $songName or artist1/artist2 + var hasCommonWord = false + + val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: "" + val trackNameWords = trackName.toLowerCase().split(" ") + + for (nameWord in trackNameWords){ + if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true + } + + // Skip this Result if No Word is Common in Name + if (!hasCommonWord) { + Log.i("YT Api Removing", result.toString()) + continue + } + + + // Find artist match + // Will Be Using Fuzzy Search Because YT Spelling might be mucked up + // match = (no of artist names in result) / (no. of artist names on spotify) * 100 + var artistMatchNumber = 0 + + if(result.type == "Song"){ + for (artist in trackArtists){ + if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase()) > 85) + artistMatchNumber++ + } + }else{//i.e. is a Video + for (artist in trackArtists) { + if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85) + artistMatchNumber++ + } + } + + if(artistMatchNumber == 0) { + Log.i("YT Api Removing", result.toString()) + continue + } + + val artistMatch = (artistMatchNumber / trackArtists.size ) * 100 + + // Duration Match + /*! time match = 100 - (delta(duration)**2 / original duration * 100) + ! difference in song duration (delta) is usually of the magnitude of a few + ! seconds, we need to amplify the delta if it is to have any meaningful impact + ! wen we calculate the avg match value*/ + val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60) + ?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0) + ?.minus(trackDurationSec)?.absoluteValue ?: 0 + val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat()) + val durationMatch = 100 - (nonMatchValue*100) + + val avgMatch = (artistMatch + durationMatch)/2 + linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt() + } + Log.i("YT Api Result", "$trackName - $linksWithMatchValue") + return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap() +} diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt index a630a217..f2448cbf 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/DownloadObject.kt @@ -18,12 +18,35 @@ 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 @Parcelize data class DownloadObject( - var ytVideo: YTTrack?=null, - var track: Track?=null, - var url:String, - var outputDir:String -):Parcelable \ No newline at end of file + var trackDetails: TrackDetails, + var ytVideoId:String, + var outputFile:String +):Parcelable + +@Parcelize +data class TrackDetails( + var title:String, + var artists:List, + var durationSec:Int, + var albumName:String?=null, + var year:String?=null, + var comment:String?=null, + var lyrics:String?=null, + var trackUrl:String?=null, + var albumArt: File, + var albumArtURL: String, + var source: Source, + var downloaded: DownloadStatus = DownloadStatus.NotDownloaded +):Parcelable + +enum class DownloadStatus{ + Downloaded, + Downloading, + NotDownloaded +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt b/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt new file mode 100644 index 00000000..2257df5d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/Optional.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Optional(val value: T?) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/YTTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt old mode 100755 new mode 100644 similarity index 80% rename from app/src/main/java/com/shabinder/spotiflyer/models/YTTrack.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt index 703a708b..03b588ba --- a/app/src/main/java/com/shabinder/spotiflyer/models/YTTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/YoutubeTrack.kt @@ -21,11 +21,10 @@ import android.os.Parcelable import kotlinx.android.parcel.Parcelize @Parcelize -data class YTTrack( - var id:String?, - var title:String?, - var duration:Int?, - var author:String?, - var viewCount:Long?, - var thumbnails:List? +data class YoutubeTrack( + var name: String? = null, + var type: String? = null, // Song / Video + var artist: String? = null, + var duration:String? = null, + var videoId: String? = null ):Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt new file mode 100644 index 00000000..49c27d04 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Artist.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models.gaana + +import com.squareup.moshi.Json + +data class Artist ( + val popularity : Int, + val seokey : String, + val name : String, + @Json(name = "artwork_175x175")var artworkLink :String? +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt new file mode 100644 index 00000000..f73b5510 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/CustomArtworks.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models.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, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt new file mode 100644 index 00000000..354e9e4d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaAlbum.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaAlbum ( + val tracks : List, + val count : Int, + val custom_artworks : CustomArtworks, + val release_year : Int, + val favorite_count : Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt new file mode 100644 index 00000000..17d57ac3 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistDetails.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaArtistDetails( + val artist : List, + val count : Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt new file mode 100644 index 00000000..2f31fb01 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaArtistTracks.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaArtistTracks( + val count : Int, + val tracks : List +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt new file mode 100644 index 00000000..38dc43ec --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaPlaylist.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaPlaylist ( + val tags : String?, + val modified_on : String, + val count : Int, + val created_on : String, + val favorite_count : Int, + val tracks : List, +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt new file mode 100644 index 00000000..8acbff96 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaSong.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class GaanaSong( + val tracks : List +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt new file mode 100644 index 00000000..f4a0b94d --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/GaanaTrack.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.models.gaana + +import com.shabinder.spotiflyer.models.DownloadStatus +import com.squareup.moshi.Json + +data class GaanaTrack ( + val tags : List?, + val seokey : String, + val albumseokey : String?, + val track_title : String, + val album_title : String?, + val language : String?, + val duration: Int, + @Json(name = "artwork_large") val artworkLink : String, + val artist : List, + @Json(name = "gener") val genre : List?, + 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?, + var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt new file mode 100644 index 00000000..0f4fcd21 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Genre.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class Genre ( + val genre_id : Int, + val name : String +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt new file mode 100644 index 00000000..c348a321 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/gaana/Tags.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.gaana + +data class Tags ( + val tag_id : Int, + val tag_name : String +) \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt similarity index 96% rename from app/src/main/java/com/shabinder/spotiflyer/models/Album.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt index 025a1e71..3c06e6ad 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Album.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Album.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt index 59a9ac54..23b12247 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Artist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Artist.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt similarity index 94% rename from app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt index d16ba35d..19761164 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Copyright.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Copyright.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt similarity index 96% rename from app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt index 804d438d..8fbfc49c 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Episodes.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Episodes.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt similarity index 94% rename from app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt index 8a198e09..423f21d8 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Followers.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Followers.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt similarity index 94% rename from app/src/main/java/com/shabinder/spotiflyer/models/Image.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt index 764f59ad..11bf5242 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Image.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Image.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt index 361378d4..ac00564d 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/LinkedTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/LinkedTrack.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt index 3f298934..caca876d 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectPlaylistTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectPlaylistTrack.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt index 004a79ec..98567afd 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PagingObjectTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PagingObjectTrack.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt similarity index 96% rename from app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt index 4842ae91..1d44e64e 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Playlist.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Playlist.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import com.squareup.moshi.Json diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt index 56a5d103..f5c5cac1 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/PlaylistTrack.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/PlaylistTrack.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt new file mode 100644 index 00000000..4c925217 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Source.kt @@ -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 . + */ + +package com.shabinder.spotiflyer.models.spotify + +enum class Source { + Spotify, + YouTube, + Gaana, +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt similarity index 94% rename from app/src/main/java/com/shabinder/spotiflyer/models/Token.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt index c2fdb85b..10d35b3a 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Token.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Token.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt similarity index 88% rename from app/src/main/java/com/shabinder/spotiflyer/models/Track.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt index 9ce8893c..ecc0809e 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/Track.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/Track.kt @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -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 @@ -31,7 +32,6 @@ data class Track( var explicit: Boolean? = null, var external_urls: Map? = null, var href: String? = null, - var id: String? = null, var name: String? = null, var preview_url: String? = null, var track_number: Int = 0, @@ -40,5 +40,6 @@ data class Track( var album: Album? = null, var external_ids: Map? = null, var popularity: Int? = null, - var ytCoverUrl:String? = null, - var downloaded:String? = "notDownloaded"):Parcelable \ No newline at end of file + var downloaded: DownloadStatus = DownloadStatus.NotDownloaded +):Parcelable + diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt similarity index 96% rename from app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt index 178798dd..fa090fec 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPrivate.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPrivate.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt similarity index 95% rename from app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt rename to app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt index 06df67cd..5732dc12 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/models/UserPublic.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/models/spotify/UserPublic.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.shabinder.spotiflyer.models +package com.shabinder.spotiflyer.models.spotify import android.os.Parcelable import kotlinx.android.parcel.Parcelize diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt new file mode 100644 index 00000000..cc69047b --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/GaanaInterface.kt @@ -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 . + */ + +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 + + /* + * 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 + + /* + * 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 + + /* + * 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 + /* + * 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 + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/SpotifyInterface.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt similarity index 54% rename from app/src/main/java/com/shabinder/spotiflyer/utils/SpotifyInterface.kt rename to app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt index 4758f80c..fcf469b5 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/SpotifyInterface.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/SpotifyInterface.kt @@ -15,58 +15,41 @@ * along with this program. If not, see . */ -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 . -*/ - - interface SpotifyService { @GET("playlists/{playlist_id}") - suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Playlist + suspend fun getPlaylist(@Path("playlist_id") playlistId: String?): Optional @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 @GET("tracks/{id}") - suspend fun getTrack(@Path("id") trackId: String?): Track + suspend fun getTrack(@Path("id") trackId: String?): Optional @GET("episodes/{id}") - suspend fun getEpisode(@Path("id") episodeId: String?): Track + suspend fun getEpisode(@Path("id") episodeId: String?): Optional @GET("shows/{id}") - suspend fun getShow(@Path("id") showId: String?): Track + suspend fun getShow(@Path("id") showId: String?): Optional @GET("albums/{id}") - suspend fun getAlbum(@Path("id") albumId: String?): Album + suspend fun getAlbum(@Path("id") albumId: String?): Optional } 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 } diff --git a/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt b/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt new file mode 100644 index 00000000..e321d19a --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/networking/YoutubeMusicApi.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.networking + +import com.beust.klaxon.JsonObject +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + + +const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30" + +interface YoutubeMusicApi { + + @Headers("Content-Type: application/json", "Referer: https://music.youtube.com/search") + @POST("search?alt=json&key=$apiKey") + fun getYoutubeMusicResponse(@Body text: String): Call +} + +fun makeJsonBody(query: String):JsonObject{ + val client = JsonObject() + client["clientName"] = "WEB_REMIX" + client["clientVersion"] = "0.1" + + val context = JsonObject() + context["client"] = client + + val mainObject = JsonObject() + mainObject["context"] = context + mainObject["query"] = query + + return mainObject +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt index 696ad40d..f2cdd643 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/DownloadRecordAdapter.kt @@ -25,6 +25,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.databinding.DownloadRecordItemBinding +import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.ui.downloadrecord.DownloadRecordFragmentDirections import com.shabinder.spotiflyer.utils.bindImage import kotlinx.coroutines.CoroutineScope @@ -34,30 +35,43 @@ import kotlinx.coroutines.launch class DownloadRecordAdapter: ListAdapter(DownloadRecordDiffCallback()) { private val adapterScope = CoroutineScope(Dispatchers.Default) + //Remember To change when Submitting a Different List / Or Use New Submit List Function + var source:Source = Source.Spotify override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent.context) - val binding =DownloadRecordItemBinding.inflate(layoutInflater) + val binding = DownloadRecordItemBinding.inflate(layoutInflater) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) adapterScope.launch { - bindImage(holder.binding.coverUrl,item.coverUrl) + bindImage(holder.binding.coverUrl,item.coverUrl,source) } holder.binding.itemName.text = item.name holder.binding.totalItems.text = "Tracks: ${item.totalFiles}" holder.binding.type.text = item.type holder.binding.btnAction.setOnClickListener { - if (item.link.contains("spotify",true)){ - it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link))) - }else if(item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) ){ - it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link)) - } + when { + item.link.contains("spotify",true) -> { + it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToSpotifyFragment((item.link))) + } + item.link.contains("youtube.com",true) || item.link.contains("youtu.be",true) -> { + it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToYoutubeFragment(item.link)) + } + item.link.contains("gaana",true) -> { + it.findNavController().navigate(DownloadRecordFragmentDirections.actionDownloadRecordToGaanaFragment((item.link))) + } + } } } class ViewHolder(val binding: DownloadRecordItemBinding) : RecyclerView.ViewHolder(binding.root) + + fun submitList(list: MutableList?,source: Source) { + super.submitList(list) + this.source = source + } } class DownloadRecordDiffCallback: DiffUtil.ItemCallback(){ diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/SpotifyTrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/SpotifyTrackListAdapter.kt deleted file mode 100755 index ec6af4e4..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/SpotifyTrackListAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2020 Shabinder Singh - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.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 androidx.recyclerview.widget.RecyclerView -import com.github.kiulian.downloader.YoutubeDownloader -import com.shabinder.spotiflyer.R -import com.shabinder.spotiflyer.databinding.TrackListItemBinding -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.context -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper.downloadAllTracks -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.ui.spotify.SpotifyViewModel -import com.shabinder.spotiflyer.utils.bindImage -import com.shabinder.spotiflyer.utils.rotateAnim -import kotlinx.coroutines.launch - - -class SpotifyTrackListAdapter: ListAdapter(SpotifyTrackDiffCallback()) { - - var spotifyViewModel : SpotifyViewModel? = null - var isAlbum:Boolean = false - var ytDownloader: YoutubeDownloader? = null - - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - val binding = TrackListItemBinding.inflate(layoutInflater,parent,false) - return ViewHolder(binding) - } - - 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) - } - } - - 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) { - "Downloaded" -> { - holder.binding.btnDownload.setImageResource(R.drawable.ic_tick) - holder.binding.btnDownload.clearAnimation() - } - "Downloading" -> { - holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) - rotateAnim(holder.binding.btnDownload) - } - "notDownloaded" -> { - holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow) - holder.binding.btnDownload.clearAnimation() - holder.binding.btnDownload.setOnClickListener{ - Toast.makeText(context,"Starting Download",Toast.LENGTH_SHORT).show() - holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) - rotateAnim(it) - item.downloaded = "Downloading" - spotifyViewModel!!.uiScope.launch { - val itemList = mutableListOf() - itemList.add(item) - downloadAllTracks(spotifyViewModel!!.folderType,spotifyViewModel!!.subFolder,itemList,ytDownloader) - } - notifyItemChanged(position)//start showing anim! - } - } - } - } - class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root) -} - -class SpotifyTrackDiffCallback: DiffUtil.ItemCallback(){ - 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt new file mode 100755 index 00000000..a2f4905f --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/TrackListAdapter.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.recyclerView + +import android.view.LayoutInflater +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.utils.* +import kotlinx.coroutines.launch + +class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter(TrackDiffCallback()) { + + var source:Source =Source.Spotify + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = TrackListItemBinding.inflate(layoutInflater,parent,false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{ + viewModel.uiScope.launch { + bindImage(holder.binding.imageUrl,item.albumArtURL, source) + } + } + + 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 + } + showMessage("Processing!") + holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) + rotateAnim(it) + item.downloaded = DownloadStatus.Downloading + when(source){ + Source.YouTube -> { + viewModel.uiScope.launch { + YTDownloadHelper.downloadYTTracks( + viewModel.folderType, + viewModel.subFolder, + listOf(item) + ) + } + } + else -> { + viewModel.uiScope.launch { + DownloadHelper.downloadAllTracks( + viewModel.folderType, + viewModel.subFolder, + listOf(item) + ) + } + } + } + notifyItemChanged(position)//start showing anim! + } + } + } + + holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}" + holder.binding.artist.text = "${item.artists.get(0)}..." + holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec" + } + + class ViewHolder(val binding: TrackListItemBinding) : RecyclerView.ViewHolder(binding.root) + + fun submitList(list: MutableList?, source: Source) { + super.submitList(list) + this.source = source + } +} +class TrackDiffCallback: DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean { + return oldItem.title == newItem.title + } + + override fun areContentsTheSame(oldItem: TrackDetails, newItem: TrackDetails): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/YoutubeTrackListAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/recyclerView/YoutubeTrackListAdapter.kt deleted file mode 100755 index d3e5e224..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/recyclerView/YoutubeTrackListAdapter.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2020 Shabinder Singh - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.recyclerView - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import com.github.kiulian.downloader.model.formats.Format -import com.shabinder.spotiflyer.databinding.TrackListItemBinding -import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.utils.bindImage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class YoutubeTrackListAdapter: ListAdapter(YouTubeTrackDiffCallback()) { - - var format:Format? = null - private val adapterScope = CoroutineScope(Dispatchers.Default) - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): SpotifyTrackListAdapter.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) - } - - override fun onBindViewHolder(holder: SpotifyTrackListAdapter.ViewHolder, position: Int) { - val item = getItem(position) - if(itemCount == 1){ - holder.binding.imageUrl.visibility = View.GONE}else{ - adapterScope.launch { - bindImage(holder.binding.imageUrl, item.ytCoverUrl) - } - } - - 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" - holder.binding.btnDownload.setOnClickListener{ - adapterScope.launch { - YTDownloadHelper.downloadFile(null,"YT_Downloads",item,format) - } - } - } -} -class YouTubeTrackDiffCallback: DiffUtil.ItemCallback(){ - override fun areItemsTheSame(oldItem: Track, newItem: Track): Boolean { - return oldItem.name == newItem.name - } - - override fun areContentsTheSame(oldItem: Track, newItem: Track): Boolean { - return oldItem == newItem - } -} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/samples/response examples.txt b/app/src/main/java/com/shabinder/spotiflyer/samples/response examples.txt index b181b4b1..750587d1 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/samples/response examples.txt +++ b/app/src/main/java/com/shabinder/spotiflyer/samples/response examples.txt @@ -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 diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordFragment.kt index e6e60b89..97150b71 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordFragment.kt @@ -21,12 +21,11 @@ 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.google.android.material.tabs.TabLayout -import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.databinding.DownloadRecordFragmentBinding +import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.recyclerView.DownloadRecordAdapter import dagger.hilt.android.AndroidEntryPoint @@ -41,43 +40,49 @@ class DownloadRecordFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate(inflater,R.layout.download_record_fragment,container,false) + binding = DownloadRecordFragmentBinding.inflate(inflater,container,false) downloadRecordViewModel = ViewModelProvider(this).get(DownloadRecordViewModel::class.java) adapter = DownloadRecordAdapter() binding.downloadRecordList.adapter = adapter downloadRecordViewModel.downloadRecordList.observe(viewLifecycleOwner, { if(it.isNotEmpty()){ - downloadRecordViewModel.spotifyList = mutableListOf() - downloadRecordViewModel.ytList = mutableListOf() + resetLists() for (downloadRecord in it) { - if(downloadRecord.link.contains("spotify",true)) downloadRecordViewModel.spotifyList.add(downloadRecord) - else downloadRecordViewModel.ytList.add(downloadRecord) + when{ + downloadRecord.link.contains("spotify",true) -> downloadRecordViewModel.spotifyList.add(downloadRecord) + downloadRecord.link.contains("gaana",true) -> downloadRecordViewModel.gaanaList.add(downloadRecord) + else -> downloadRecordViewModel.ytList.add(downloadRecord) + } + } + when(binding.tabLayout.selectedTabPosition){ + 0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify) + 1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana) + 2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube) } - if(binding.tabLayout.selectedTabPosition == 0) adapter.submitList(downloadRecordViewModel.spotifyList) - else adapter.submitList(downloadRecordViewModel.ytList) } }) binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab?) { - if(tab?.text == "Spotify"){ - adapter.submitList(downloadRecordViewModel.spotifyList) - } else adapter.submitList(downloadRecordViewModel.ytList) - } - - override fun onTabReselected(tab: TabLayout.Tab?) { - // Handle tab reselect - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { - // Handle tab unselected + when(tab?.position){ + 0-> adapter.submitList(downloadRecordViewModel.spotifyList,Source.Spotify) + 1-> adapter.submitList(downloadRecordViewModel.gaanaList,Source.Gaana) + 2-> adapter.submitList(downloadRecordViewModel.ytList,Source.YouTube) + } } + override fun onTabReselected(tab: TabLayout.Tab?) {} + override fun onTabUnselected(tab: TabLayout.Tab?) {} }) return binding.root } + private fun resetLists() { + downloadRecordViewModel.spotifyList = mutableListOf() + downloadRecordViewModel.ytList = mutableListOf() + downloadRecordViewModel.gaanaList = mutableListOf() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordViewModel.kt index 7f9cc1a7..d985d4a3 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/downloadrecord/DownloadRecordViewModel.kt @@ -32,6 +32,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data private var viewModelJob = Job() private val uiScope = CoroutineScope(Dispatchers.Default + viewModelJob) var spotifyList = mutableListOf() + var gaanaList = mutableListOf() var ytList = mutableListOf() val downloadRecordList = MutableLiveData>().apply { value = mutableListOf() @@ -40,6 +41,7 @@ class DownloadRecordViewModel @ViewModelInject constructor(val databaseDAO: Data init { getDownloadRecordList() } + private fun getDownloadRecordList() { uiScope.launch { downloadRecordList.postValue(databaseDAO.getRecord().toMutableList()) diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt new file mode 100644 index 00000000..330e741a --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaFragment.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.gaana + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.SimpleItemAnimator +import com.shabinder.spotiflyer.SharedViewModel +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.networking.GaanaInterface +import com.shabinder.spotiflyer.networking.YoutubeMusicApi +import com.shabinder.spotiflyer.recyclerView.TrackListAdapter +import com.shabinder.spotiflyer.utils.* +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class GaanaFragment : TrackListFragment() { + + @Inject lateinit var youtubeMusicApi: YoutubeMusicApi + @Inject lateinit var gaanaInterface: GaanaInterface + override lateinit var viewModel: GaanaViewModel + override lateinit var adapter: TrackListAdapter + override var source: Source = Source.Gaana + override val args: GaanaFragmentArgs by navArgs() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + + initializeAll() + + 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() + viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) } + //Appending Source + urlList.add("gaana") + 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 + } + + /** + * Basic Initialization + **/ + private fun initializeAll() { + sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) + viewModel = ViewModelProvider(this).get(GaanaViewModel::class.java) + viewModel.gaanaInterface = gaanaInterface + adapter = TrackListAdapter(viewModel) + DownloadHelper.youtubeMusicApi = youtubeMusicApi + DownloadHelper.sharedViewModel = sharedViewModel + DownloadHelper.statusBar = binding.statusBar + binding.trackList.adapter = adapter + (binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt new file mode 100644 index 00000000..15f83620 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/gaana/GaanaViewModel.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.ui.gaana + +import android.os.Environment +import android.util.Log +import androidx.hilt.lifecycle.ViewModelInject +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.gaana.* +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.networking.GaanaInterface +import com.shabinder.spotiflyer.utils.Provider +import com.shabinder.spotiflyer.utils.TrackListViewModel +import com.shabinder.spotiflyer.utils.finalOutputDir +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + +class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){ + + override var folderType:String = "" + override var subFolder:String = "" + var gaanaInterface : GaanaInterface? = null + val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" + + fun gaanaSearch(type:String,link:String){ + when(type){ + "song" -> { + uiScope.launch { + getGaanaSong(link)?.tracks?.firstOrNull()?.also { + folderType = "Tracks" + if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!! + it.downloaded = DownloadStatus.Downloaded + } + trackList.value = listOf(it).toTrackDetailsList() + title.value = it.track_title + coverUrl.value = it.artworkLink + withContext(Dispatchers.IO){ + databaseDAO.insert( + DownloadRecord( + type = "Track", + name = title.value!!, + link = "https://gaana.com/$type/$link", + coverUrl = coverUrl.value!!, + totalFiles = 1, + downloaded = it.downloaded == DownloadStatus.Downloaded, + directory = finalOutputDir(it.track_title,folderType,subFolder) + ) + ) + } + } + } + } + "album" -> { + uiScope.launch { + getGaanaAlbum(link)?.also { + folderType = "Albums" + subFolder = link + it.tracks.forEach { track -> + if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! + track.downloaded = DownloadStatus.Downloaded + } + } + trackList.value = it.tracks.toTrackDetailsList() + title.value = link + coverUrl.value = it.custom_artworks.size_480p + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "Album", + name = title.value!!, + link = "https://gaana.com/$type/$link", + coverUrl = coverUrl.value.toString(), + totalFiles = trackList.value?.size ?: 0, + downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, + directory = finalOutputDir(type = folderType,subFolder = subFolder) + )) + } + } + } + } + "playlist" -> { + uiScope.launch { + getGaanaPlaylist(link)?.also { + folderType = "Playlists" + subFolder = link + it.tracks.forEach {track -> + if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! + track.downloaded = DownloadStatus.Downloaded + } + } + trackList.value = it.tracks.toTrackDetailsList() + title.value = link + //coverUrl.value = "TODO" + coverUrl.value = gaanaPlaceholderImageUrl + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "Playlist", + name = title.value.toString(), + link = "https://gaana.com/$type/$link", + coverUrl = coverUrl.value.toString(), + totalFiles = it.tracks.size, + downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, + directory = finalOutputDir(type = folderType,subFolder = subFolder) + )) + } + } + } + } + "artist" -> { + uiScope.launch { + folderType = "Artist" + subFolder = link + val artistDetails = getGaanaArtistDetails(link)?.artist?.firstOrNull()?.also { + title.value = it.name + coverUrl.value = it.artworkLink + } + getGaanaArtistTracks(link)?.also { + it.tracks.forEach {track -> + if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! + track.downloaded = DownloadStatus.Downloaded + } + } + trackList.value = it.tracks.toTrackDetailsList() + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "Artist", + name = artistDetails?.name ?: link, + link = "https://gaana.com/$type/$link", + coverUrl = coverUrl.value.toString(), + totalFiles = trackList.value?.size ?: 0, + downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, + directory = finalOutputDir(type = folderType,subFolder = subFolder) + )) + } + } + } + } + } + } + + + private fun List.toTrackDetailsList() = this.map { + TrackDetails( + title = it.track_title, + artists = it.artist.map { artist -> artist?.name.toString() }, + durationSec = it.duration, + albumArt = File( + Environment.getExternalStorageDirectory(), + Provider.defaultDir +".Images/" + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"), + albumName = it.album_title, + year = it.release_date, + comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}", + trackUrl = it.lyrics_url, + downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, + source = Source.Gaana, + albumArtURL = it.artworkLink + ) + }.toMutableList() + + private suspend fun getGaanaSong(songLink:String): GaanaSong?{ + Log.i("Requesting","https://gaana.com/song/$songLink") + return gaanaInterface?.getGaanaSong(seokey = songLink)?.value + } + private suspend fun getGaanaAlbum(albumLink:String): GaanaAlbum?{ + Log.i("Requesting","https://gaana.com/album/$albumLink") + return gaanaInterface?.getGaanaAlbum(seokey = albumLink)?.value + } + private suspend fun getGaanaPlaylist(link:String): GaanaPlaylist?{ + Log.i("Requesting","https://gaana.com/playlist/$link") + return gaanaInterface?.getGaanaPlaylist(seokey = link)?.value + } + private suspend fun getGaanaArtistDetails(link:String): GaanaArtistDetails?{ + Log.i("Requesting","https://gaana.com/artist/$link") + return gaanaInterface?.getGaanaArtistDetails(seokey = link)?.value + } + private suspend fun getGaanaArtistTracks(link:String,limit:Int = 50): GaanaArtistTracks?{ + Log.i("Requesting","Tracks of: https://gaana.com/artist/$link") + return gaanaInterface?.getGaanaArtistTracks(seokey = link,limit = limit)?.value + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt index b2accaf3..7a0033e8 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/mainfragment/MainFragment.kt @@ -17,22 +17,19 @@ package com.shabinder.spotiflyer.ui.mainfragment -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri import android.os.Bundle 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.* import com.shreyaspatil.easyupipayment.EasyUpiPayment import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -53,123 +50,88 @@ class MainFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false) + binding = MainFragmentBinding.inflate(inflater,container,false) initializeAll() - binding.btnSearch.setOnClickListener { + if(!isOnline()){ + showNoConnectionAlert() + return@setOnClickListener + } val link = binding.linkSearch.text.toString() - if (link.contains("spotify",true)){ - 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()} + when{ + //SPOTIFY + link.contains("spotify",true) -> { + if(sharedViewModel.spotifyService.value == null){//Authentication pending!! + (activity as MainActivity).authenticateSpotify() + } + findNavController().navigate(MainFragmentDirections.actionMainFragmentToSpotifyFragment(link)) + } + + //YOUTUBE + link.contains("youtube.com",true) || link.contains("youtu.be",true) -> { + findNavController().navigate(MainFragmentDirections.actionMainFragmentToYoutubeFragment(link)) + } + + //GAANA + link.contains("gaana",true) -> { + findNavController().navigate(MainFragmentDirections.actionMainFragmentToGaanaFragment(link)) + } + + else -> showMessage("Link is Not Valid",true) + } } handleIntent() return binding.root } - - private fun initializeAll() { - mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) - sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) - openYTButton() - openSpotifyButton() - openGithubButton() - openInstaButton() - openLinkedInButton() - historyButton() - binding.usage.text = usageText() - binding.btnDonate.setOnClickListener { - easyUpiPayment.startPayment() - } - } - - 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) { - while (sharedViewModel.accessToken.value == "") { + 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() - sharedViewModel.intentString.value = "" - } + } + + withContext(Dispatchers.Main){ + binding.linkSearch.setText(sharedViewModel.intentString.value) + binding.btnSearch.performClick() + //Intent Consumed + sharedViewModel.intentString.value = null } } + } }) } - /** - * Implementing buttons - **/ - private fun openSpotifyButton() { - val manager: PackageManager = requireActivity().packageManager - try { - val i = manager.getLaunchIntentForPackage("com.spotify.music") - ?: throw PackageManager.NameNotFoundException() - i.addCategory(Intent.CATEGORY_LAUNCHER) - binding.btnSpotify.setOnClickListener { startActivity(i) } - } catch (e: PackageManager.NameNotFoundException) { - val uri: Uri = - Uri.parse("http://open.spotify.com") - val intent = Intent(Intent.ACTION_VIEW, uri) - binding.btnSpotify.setOnClickListener { - startActivity(intent) + private fun initializeAll() { + mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java) + sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) + binding.apply { + btnGaana.openPlatformOnClick("com.gaana","http://gaana.com") + btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com") + btnYoutube.openPlatformOnClick("com.google.android.youtube","http://m.youtube.com") + btnGithub.openPlatformOnClick("http://github.com/Shabinder/SpotiFlyer") + btnInsta.openPlatformOnClick("http://www.instagram.com/mr.shabinder") + btnHistory.setOnClickListener { + findNavController().navigate(MainFragmentDirections.actionMainFragmentToDownloadRecord()) + } + usage.text = usageText() + btnDonate.setOnClickListener { + easyUpiPayment.startPayment() } } } - private fun openYTButton() { - val manager: PackageManager = requireActivity().packageManager - try { - val i = manager.getLaunchIntentForPackage("com.google.android.youtube") - ?: throw PackageManager.NameNotFoundException() - i.addCategory(Intent.CATEGORY_LAUNCHER) - binding.btnYoutube.setOnClickListener { startActivity(i) } - } catch (e: PackageManager.NameNotFoundException) { - val uri: Uri = - Uri.parse("http://m.youtube.com") - val intent = Intent(Intent.ACTION_VIEW, uri) - binding.btnYoutube.setOnClickListener { - startActivity(intent) - } - } - } - private fun openGithubButton() { - val uri: Uri = - Uri.parse("http://github.com/Shabinder/SpotiFlyer") - val intent = Intent(Intent.ACTION_VIEW, uri) - binding.btnGithubSpotify.setOnClickListener { - startActivity(intent) - } - } - private fun openLinkedInButton() { - val uri: Uri = - Uri.parse("https://in.linkedin.com/in/shabinder") - val intent = Intent(Intent.ACTION_VIEW, uri) - binding.btnLinkedin.setOnClickListener { - startActivity(intent) - } - } - private fun openInstaButton() { - val uri: Uri = - Uri.parse("http://www.instagram.com/mr.shabinder") - val intent = Intent(Intent.ACTION_VIEW, uri) - binding.developerInstaSpotify.setOnClickListener { - startActivity(intent) - } - } + private fun usageText(): SpannableStringBuilder { return SpannableStringBuilder() .append(getText(R.string.d_one)).append("\n") diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt index 9e2835b3..465d7eb6 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyFragment.kt @@ -18,307 +18,124 @@ package com.shabinder.spotiflyer.ui.spotify import android.annotation.SuppressLint -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.webkit.WebView -import android.widget.Toast -import androidx.core.net.toUri -import androidx.databinding.DataBindingUtil -import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.SimpleItemAnimator -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target -import com.github.kiulian.downloader.YoutubeDownloader -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.models.Track -import com.shabinder.spotiflyer.recyclerView.SpotifyTrackListAdapter -import com.shabinder.spotiflyer.utils.bindImage -import com.shabinder.spotiflyer.utils.copyTo -import com.shabinder.spotiflyer.utils.rotateAnim +import com.shabinder.spotiflyer.downloadHelper.DownloadHelper +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.networking.YoutubeMusicApi +import com.shabinder.spotiflyer.recyclerView.TrackListAdapter +import com.shabinder.spotiflyer.utils.* +import com.shabinder.spotiflyer.utils.Provider.mainActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.IOException import javax.inject.Inject -@Suppress("DEPRECATION") - @AndroidEntryPoint -class SpotifyFragment : Fragment() { - private lateinit var binding:SpotifyFragmentBinding - private lateinit var spotifyViewModel: SpotifyViewModel - private lateinit var sharedViewModel: SharedViewModel - private lateinit var adapterSpotify:SpotifyTrackListAdapter - @Inject lateinit var ytDownloader:YoutubeDownloader - private var webView: WebView? = null - private var intentFilter:IntentFilter? = null - private var updateUIReceiver: BroadcastReceiver? = null +class SpotifyFragment : TrackListFragment() { + @Inject lateinit var youtubeMusicApi: YoutubeMusicApi + override lateinit var viewModel: SpotifyViewModel + override lateinit var adapter: TrackListAdapter + override var source: Source = Source.Spotify + override val args: SpotifyFragmentArgs by navArgs() @SuppressLint("SetJavaScriptEnabled") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = DataBindingUtil.inflate(inflater,R.layout.spotify_fragment,container,false) - adapterSpotify = SpotifyTrackListAdapter() + super.onCreateView(inflater, container, savedInstanceState) initializeAll() - initializeLiveDataObservers() - initializeBroadcast() - val args = SpotifyFragmentArgs.fromBundle(requireArguments()) - val spotifyLink = args.link + val spotifyLink = args.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(isOnline()) mainActivity.authenticateSpotify() } - if(!isOnline()){//Device Offline - sharedViewModel.showAlertDialog(resources,requireContext()) - }else if (type == "Error" || link == "Error") {//Incorrect Link - showToast("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!") + + when{ + type == "Error" || link == "Error" -> { + showMessage("Please Check Your Link!") + mainActivity.onBackPressed() } - else{ - spotifyViewModel.spotifySearch(type,link) - if(type=="album")adapterSpotify.isAlbum = true - binding.btnDownloadAllSpotify.setOnClickListener { - for (track in spotifyViewModel.trackList.value!!){ - if(track.downloaded != "Downloaded"){ - track.downloaded = "Downloading" + else -> { + if(type == "episode" || type == "show"){//TODO Implementation + showMessage("Implementing Soon, Stay Tuned!") + } + else{ + this.viewModel.spotifySearch(type,link) + + binding.btnDownloadAll.setOnClickListener { + if(!isOnline()){ + showNoConnectionAlert() + return@setOnClickListener } - } - binding.btnDownloadAllSpotify.visibility = View.GONE - binding.downloadingFabSpotify.visibility = View.VISIBLE + binding.btnDownloadAll.visibility = View.GONE + binding.downloadingFab.visibility = View.VISIBLE - - rotateAnim(binding.downloadingFabSpotify) - for (track in spotifyViewModel.trackList.value!!){ - if(track.downloaded != "Downloaded"){ - adapterSpotify.notifyItemChanged(spotifyViewModel.trackList.value!!.indexOf(track)) + rotateAnim(binding.downloadingFab) + for (track in this.viewModel.trackList.value ?: listOf()){ + if(track.downloaded != DownloadStatus.Downloaded){ + track.downloaded = DownloadStatus.Downloading + adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track)) + } + } + showMessage("Processing!") + sharedViewModel.uiScope.launch(Dispatchers.Default){ + val urlList = arrayListOf() + this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) } + //Appending Source + urlList.add("spotify") + loadAllImages( + requireActivity(), + urlList + ) + } + this.viewModel.uiScope.launch { + val finalList = viewModel.trackList.value + if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") + DownloadHelper.downloadAllTracks( + viewModel.folderType, + viewModel.subFolder, + finalList ?: listOf(), + ) } - } - showToast("Starting Download in Few Seconds") - spotifyViewModel.uiScope.launch(Dispatchers.Default){loadAllImages(spotifyViewModel.trackList.value!!)} - spotifyViewModel.uiScope.launch { - SpotifyDownloadHelper.downloadAllTracks( - spotifyViewModel.folderType, - spotifyViewModel.subFolder, - spotifyViewModel.trackList.value!!, - ytDownloader - ) } } } } + return binding.root } - override fun onResume() { - super.onResume() - initializeBroadcast() - } - - private fun initializeBroadcast() { - intentFilter = IntentFilter() - intentFilter?.addAction("track_download_completed") - - updateUIReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - //UI update here - if (intent != null){ - val track = intent.getParcelableExtra("track") - track?.let { - val position: Int = spotifyViewModel.trackList.value?.indexOf(track)!! - Log.i("Track","Download Completed Intent :$position") - track.downloaded = "Downloaded" - if(position != -1) { - spotifyViewModel.trackList.value?.set(position, track) - adapterSpotify.notifyItemChanged(position) - checkIfAllDownloaded() - } - } - } - } - } - requireActivity().registerReceiver(updateUIReceiver, intentFilter) - } - - override fun onPause() { - super.onPause() - requireActivity().unregisterReceiver(updateUIReceiver) - } - - /** - *Live Data Observers - **/ - private fun initializeLiveDataObservers() { - /** - * CoverUrl Binding Observer! - **/ - spotifyViewModel.coverUrl.observe(viewLifecycleOwner, { - if(it!="Loading") bindImage(binding.spotifyCoverImage,it) - }) - - /** - * TrackList Binding Observer! - **/ - spotifyViewModel.trackList.observe(viewLifecycleOwner, { - if (it.isNotEmpty()){ - Log.i("SpotifyFragment","TrackList Updated") - adapterConfig(it) - checkIfAllDownloaded() - } - }) - - /** - * Title Binding Observer! - **/ - spotifyViewModel.title.observe(viewLifecycleOwner, { - binding.titleViewSpotify.text = it - }) - - sharedViewModel.intentString.observe(viewLifecycleOwner,{ - //Waiting for Authentication to Finish with Spotify()Access Token Observe - if(it != "" && it!=SpotifyFragmentArgs.fromBundle(requireArguments()).link){ - //New Intent Received , Time TO RELOAD - (activity as MainActivity).onBackPressed() - } - }) - } - - private fun checkIfAllDownloaded() { - if(!spotifyViewModel.trackList.value!!.any { it.downloaded != "Downloaded" }){ - //All Tracks Downloaded - binding.btnDownloadAllSpotify.visibility = View.GONE - binding.downloadingFabSpotify.apply{ - setImageResource(R.drawable.ic_tick) - visibility = View.VISIBLE - clearAnimation() - keepScreenOn = false - } - } - } - /** * Basic Initialization **/ private fun initializeAll() { - webView = binding.webViewSpotify - sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) - spotifyViewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java) - sharedViewModel.spotifyService.observe(viewLifecycleOwner, Observer { - spotifyViewModel.spotifyService = it + this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java) + adapter = TrackListAdapter(this.viewModel) + sharedViewModel.spotifyService.observe(viewLifecycleOwner, { + this.viewModel.spotifyService = it }) - SpotifyDownloadHelper.webView = binding.webViewSpotify - SpotifyDownloadHelper.context = requireContext() - SpotifyDownloadHelper.spotifyViewModel = spotifyViewModel - SpotifyDownloadHelper.statusBar = binding.StatusBarSpotify - binding.trackListSpotify.adapter = adapterSpotify - (binding.trackListSpotify.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - } - - /** - * Function to fetch all Images for using in mp3 tag. - **/ - private suspend fun loadAllImages(trackList: List) { - trackList.forEach { - val imgUrl = it.album?.images?.get(0)?.url - imgUrl?.let { - val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() - Glide - .with(requireContext()) - .asFile() - .load(imgUri) - .listener(object: RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - Log.i("Glide","LoadFailed") - return false - } - - override fun onResourceReady( - resource: File?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - sharedViewModel.uiScope.launch { - withContext(Dispatchers.IO){ - try { - val file = File( - Environment.getExternalStorageDirectory(), - SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg" - ) - resource?.copyTo(file) - } catch (e: IOException) { - e.printStackTrace() - } - } - } - return false - } - }).submit() - } - } - } - - /** - * Configure Recycler View Adapter - **/ - private fun adapterConfig(trackList: List){ - adapterSpotify.ytDownloader = ytDownloader - adapterSpotify.spotifyViewModel = spotifyViewModel - adapterSpotify.submitList(trackList) - } - - - /** - * Util. Function to create toasts! - **/ - private fun showToast(message:String){ - Toast.makeText(context,message,Toast.LENGTH_SHORT).show() - } - - /** - * Util. Function To Check Connection Status - **/ - private fun isOnline(): Boolean { - val cm = - requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val netInfo = cm.activeNetworkInfo - return netInfo != null && netInfo.isConnectedOrConnecting + DownloadHelper.youtubeMusicApi = youtubeMusicApi + DownloadHelper.sharedViewModel = sharedViewModel + DownloadHelper.statusBar = binding.statusBar + binding.trackList.adapter = adapter + (binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt index 6a3a9082..705166f9 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/spotify/SpotifyViewModel.kt @@ -17,57 +17,53 @@ 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.* -import com.shabinder.spotiflyer.utils.SpotifyService +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.Provider +import com.shabinder.spotiflyer.utils.TrackListViewModel 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) : TrackListViewModel(){ + + override var folderType:String = "" + override var subFolder:String = "" - var folderType:String = "" - var subFolder:String = "" - var trackList = MutableLiveData>() - private val loading = "Loading" - var title = MutableLiveData().apply { value = loading } - var coverUrl = MutableLiveData().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" -> { uiScope.launch { - val trackObject = getTrackDetails(link) - folderType = "Tracks" - val tempTrackList = mutableListOf() - if(File(finalOutputDir(trackObject?.name!!,folderType,subFolder)).exists()){//Download Already Present!! - trackObject.downloaded = "Downloaded" - } - tempTrackList.add(trackObject) - trackList.value = tempTrackList - title.value = trackObject.name - coverUrl.value = trackObject.album!!.images?.get(0)!!.url!! - withContext(Dispatchers.IO){ - databaseDAO.insert(DownloadRecord( - type = "Track", - name = title.value!!, - link = "https://open.spotify.com/$type/$link", - coverUrl = coverUrl.value!!, - totalFiles = tempTrackList.size, - downloaded = trackObject.downloaded =="Downloaded", - directory = finalOutputDir(trackObject.name!!,folderType,subFolder) - )) + getTrackDetails(link)?.also { + folderType = "Tracks" + if(File(finalOutputDir(it.name,folderType,subFolder)).exists()){//Download Already Present!! + it.downloaded = DownloadStatus.Downloaded + } + trackList.value = listOf(it).toTrackDetailsList() + title.value = it.name + coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "Track", + name = title.value!!, + link = "https://open.spotify.com/$type/$link", + coverUrl = coverUrl.value!!, + totalFiles = 1, + downloaded = it.downloaded == DownloadStatus.Downloaded, + directory = finalOutputDir(it.name,folderType,subFolder) + )) + } } } } @@ -77,25 +73,23 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO val albumObject = getAlbumDetails(link) folderType = "Albums" subFolder = albumObject?.name.toString() - val tempTrackList = mutableListOf() albumObject?.tracks?.items?.forEach { if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!! - it.downloaded = "Downloaded" + it.downloaded = DownloadStatus.Downloaded } - it.album = Album(images = listOf(Image(url = albumObject.images?.get(0)?.url))) - tempTrackList.add(it) + it.album = Album(images = listOf(Image(url = albumObject.images?.elementAtOrNull(1)?.url ?: albumObject.images?.elementAtOrNull(0)?.url ))) } - trackList.value = tempTrackList + trackList.value = albumObject?.tracks?.items?.toTrackDetailsList() title.value = albumObject?.name - coverUrl.value = albumObject?.images?.get(0)?.url + coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url ?: albumObject?.images?.elementAtOrNull(0)?.url withContext(Dispatchers.IO){ databaseDAO.insert(DownloadRecord( type = "Album", name = title.value!!, link = "https://open.spotify.com/$type/$link", coverUrl = coverUrl.value.toString(), - totalFiles = tempTrackList.size, - downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size, + totalFiles = trackList.value?.size ?: 0, + downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, directory = finalOutputDir(type = folderType,subFolder = subFolder) )) } @@ -112,7 +106,7 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO playlistObject?.tracks?.items?.forEach { it.track?.let { it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!! - it1.downloaded = "Downloaded" + it1.downloaded = DownloadStatus.Downloaded } tempTrackList.add(it1) } @@ -128,15 +122,15 @@ 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!! + coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url ?: playlistObject?.images?.firstOrNull()?.url.toString() withContext(Dispatchers.IO){ databaseDAO.insert(DownloadRecord( type = "Playlist", - name = title.value!!, + name = title.value.toString(), link = "https://open.spotify.com/$type/$link", - coverUrl = coverUrl.value!!, + coverUrl = coverUrl.value.toString(), totalFiles = tempTrackList.size, downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size, directory = finalOutputDir(type = folderType,subFolder = subFolder) @@ -151,26 +145,39 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO } } + @Suppress("DEPRECATION") + private fun List.toTrackDetailsList() = this.map { + TrackDetails( + title = it.name.toString(), + artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), + durationSec = (it.duration_ms/1000).toInt(), + albumArt = File( + Environment.getExternalStorageDirectory(), + Provider.defaultDir +".Images/" + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.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?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString() + ) + }.toMutableList() + 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() { - super.onCleared() - viewModelJob.cancel() - } - } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt index cc3678ad..03cb626f 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeFragment.kt @@ -21,46 +21,39 @@ import android.os.Bundle 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.navigation.fragment.navArgs 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.downloadHelper.YTDownloadHelper -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.recyclerView.YoutubeTrackListAdapter -import com.shabinder.spotiflyer.utils.bindImage +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.recyclerView.TrackListAdapter +import com.shabinder.spotiflyer.utils.* import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import javax.inject.Inject -@AndroidEntryPoint -class YoutubeFragment : Fragment() { +private const val sampleDomain2 = "youtu.be" +private const val sampleDomain1 = "youtube.com" + +@AndroidEntryPoint +class YoutubeFragment : TrackListFragment() { - private lateinit var binding:YoutubeFragmentBinding - private lateinit var youtubeViewModel: YoutubeViewModel - private lateinit var sharedViewModel: SharedViewModel - private lateinit var adapter : YoutubeTrackListAdapter - private val sampleDomain1 = "youtube.com" - private val sampleDomain2 = "youtu.be" @Inject lateinit var ytDownloader: YoutubeDownloader + override lateinit var viewModel: YoutubeViewModel + override lateinit var adapter : TrackListAdapter + override var source: Source = Source.YouTube + override val args: YoutubeFragmentArgs by navArgs() 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) - sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) - adapter = YoutubeTrackListAdapter() - YTDownloadHelper.context = requireContext() - YTDownloadHelper.statusBar = binding.StatusBarYoutube - binding.trackListYoutube.adapter = adapter - - initializeLiveDataObservers() + super.onCreateView(inflater, container, savedInstanceState) + this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) + adapter = TrackListAdapter(this.viewModel) + binding.trackList.adapter = adapter val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val link = args.link @@ -70,7 +63,11 @@ class YoutubeFragment : Fragment() { private fun youtubeSearch(linkSearch:String) { val link = linkSearch.removePrefix("https://").removePrefix("http://") - if(!link.contains("playlist",true)){ + if(link.contains("playlist",true) || link.contains("list",true)){ + // Given Link is of a Playlist + val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&") + this.viewModel.getYTPlaylist(playlistId,ytDownloader) + }else{//Given Link is of a Video var searchId = "error" if(link.contains(sampleDomain1,true) ){ searchId = link.substringAfterLast("=","error") @@ -79,57 +76,48 @@ class YoutubeFragment : Fragment() { searchId = link.substringAfterLast("/","error") } if(searchId != "error") { - youtubeViewModel.getYTTrack(searchId,ytDownloader) - binding.btnDownloadAllYoutube.setOnClickListener { - YTDownloadHelper.downloadFile(null,"YT_Downloads", - youtubeViewModel.ytTrack.value!!,youtubeViewModel.format.value) + this.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 this.viewModel.trackList.value?: listOf()){ + if(track.downloaded != DownloadStatus.Downloaded){ + track.downloaded = DownloadStatus.Downloading + adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track)) } - }else{showToast("Your Youtube Link is not of a Video!!")} - }else(showToast("Your Youtube Link is not of a Video!!")) + } + showMessage("Processing!") + sharedViewModel.uiScope.launch(Dispatchers.Default){ + val urlList = arrayListOf() + viewModel.trackList.value?.forEach { urlList.add("https://i.ytimg.com/vi/${it.albumArt.absolutePath.substringAfterLast("/") + .substringBeforeLast(".")}/hqdefault.jpg")} + //Appending Source + urlList.add("youtube") + loadAllImages( + requireActivity(), + urlList + ) + } + viewModel.uiScope.launch { + YTDownloadHelper.downloadYTTracks( + type = viewModel.folderType, + subFolder = viewModel.subFolder, + tracks = viewModel.trackList.value ?: listOf() + ) + } + } } - - private fun initializeLiveDataObservers() { - /** - * CoverUrl Binding Observer! - **/ - youtubeViewModel.coverUrl.observe(viewLifecycleOwner, Observer { - if(it!="Loading") bindImage(binding.youtubeCoverImage,it) - }) - - /** - * TrackList Binding Observer! - **/ - youtubeViewModel.ytTrack.observe(viewLifecycleOwner, Observer { - val list = mutableListOf() - list.add(it) - adapterConfig(list) - }) - - youtubeViewModel.format.observe(viewLifecycleOwner, Observer { - adapter.format = it - }) - - /** - * Title Binding Observer! - **/ - youtubeViewModel.title.observe(viewLifecycleOwner, Observer { - binding.titleViewYoutube.text = it - }) - - } - - /** - * Configure Recycler View Adapter - **/ - private fun adapterConfig(list:List){ - adapter.submitList(list) - } - - /** - * Util. Function to create toasts! - **/ - private fun showToast(message:String){ - Toast.makeText(context,message, Toast.LENGTH_SHORT).show() - } - } diff --git a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt index 48beedee..48d816aa 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/ui/youtube/YoutubeViewModel.kt @@ -17,79 +17,134 @@ package com.shabinder.spotiflyer.ui.youtube +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.github.kiulian.downloader.model.formats.Format -import com.github.kiulian.downloader.model.quality.AudioQuality import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DownloadRecord -import com.shabinder.spotiflyer.models.Artist -import com.shabinder.spotiflyer.models.Track -import com.shabinder.spotiflyer.utils.finalOutputDir -import kotlinx.coroutines.* +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.TrackDetails +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.utils.* +import com.shabinder.spotiflyer.utils.Provider.defaultDir +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) : TrackListViewModel(){ + /* + * 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 ytTrack = MutableLiveData() - val format = MutableLiveData() - private val loading = "Loading" - var title = MutableLiveData().apply { value = "\"Loading!\"" } - var coverUrl = MutableLiveData().apply { value = loading } + override var folderType = "YT_Downloads" + override var subFolder = "" - private var viewModelJob = Job() - val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) - - - fun getYTTrack(searchId:String,ytDownloader:YoutubeDownloader) { - uiScope.launch { - withContext(Dispatchers.IO){ - Log.i("YT View Model",searchId) - val video = ytDownloader.getVideo(searchId) - val detail = video?.details() - val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() - Log.i("YT View Model",detail.toString()) - ytTrack.postValue( - Track( - id = searchId, - name = name, - artists = listOf(Artist(name = detail?.author())), - duration_ms = detail?.lengthSeconds()?.times(1000)?.toLong()?:0, - ytCoverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" - )) - coverUrl.postValue("https://i.ytimg.com/vi/$searchId/maxresdefault.jpg") + fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){ + if(!isOnline())return + try{ + uiScope.launch(Dispatchers.IO) { + Log.i("YT Playlist",searchId) + val playlist = ytDownloader.getPlaylist(searchId) + val playlistDetails = playlist.details() + val name = playlistDetails.title() + subFolder = removeIllegalChars(name).toString() + val videos = playlist.videos() + coverUrl.postValue("https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg") title.postValue( - if(name?.length!! > 17){"${name.subSequence(0,16)}..."}else{name} + if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} ) - format.postValue(try { - video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format - } catch (e: IndexOutOfBoundsException) { - try { - video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format - } catch (e: IndexOutOfBoundsException) { - try { - video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format - } catch (e: IndexOutOfBoundsException) { - Log.i("YTDownloader", e.toString()) - null + this@YoutubeViewModel.trackList.postValue(videos.map { + TrackDetails( + title = it.title(), + artists = listOf(it.author().toString()), + durationSec = it.lengthSeconds(), + albumArt = File( + Environment.getExternalStorageDirectory(), + defaultDir + ".Images/" + it.videoId() + ".jpeg" + ), + source = Source.YouTube, + albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg", + downloaded = if (File( + finalOutputDir( + itemName = it.title(), + type = folderType, + subFolder = subFolder + )).exists() + ) + DownloadStatus.Downloaded + else { + DownloadStatus.NotDownloaded } - } - }) + ) + }.toMutableList()) + + withContext(Dispatchers.IO){ + databaseDAO.insert(DownloadRecord( + type = "PlayList", + name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, + link = "https://www.youtube.com/playlist?list=$searchId", + coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg", + totalFiles = videos.size, + directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder), + downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists() + )) + } + } + }catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){ + showMessage("An Error Occurred While Processing!") + } + + } + + @SuppressLint("DefaultLocale") + fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) { + if(!isOnline())return + try{ + uiScope.launch(Dispatchers.IO) { + Log.i("YT Video",searchId) + val video = ytDownloader.getVideo(searchId) + coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg") + val detail = video?.details() + val name = detail?.title()?.replace(detail.author()!!.toUpperCase(),"",true) ?: detail?.title() ?: "" + Log.i("YT View Model",detail.toString()) + this@YoutubeViewModel.trackList.postValue( + listOf( + TrackDetails( + title = name, + artists = listOf(detail?.author().toString()), + durationSec = detail?.lengthSeconds()?:0, + albumArt = File( + Environment.getExternalStorageDirectory(), + "$defaultDir.Images/$searchId.jpeg" + ), + source = Source.YouTube, + albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" + ) + ).toMutableList() + ) + title.postValue( + if(name.length > 17){"${name.subSequence(0,16)}..."}else{name} + ) + withContext(Dispatchers.IO){ databaseDAO.insert(DownloadRecord( type = "Track", name = if(name.length > 17){"${name.subSequence(0,16)}..."}else{name}, link = "https://www.youtube.com/watch?v=$searchId", - coverUrl = "https://i.ytimg.com/vi/$searchId/maxresdefault.jpg", + coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", totalFiles = 1, downloaded = false, directory = finalOutputDir(type = "YT_Downloads") )) } } + } catch (e:com.github.kiulian.downloader.YoutubeException){ + showMessage("An Error Occurred While Processing!") } } } diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt deleted file mode 100755 index b18980a7..00000000 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/BindingAdapter.kt +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2020 Shabinder Singh - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.shabinder.spotiflyer.utils - -import android.os.Environment -import android.util.Log -import android.view.View -import android.view.animation.Animation -import android.view.animation.LinearInterpolator -import android.view.animation.RotateAnimation -import android.widget.ImageView -import androidx.core.net.toUri -import androidx.databinding.BindingAdapter -import com.bumptech.glide.Glide -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.target.Target -import com.shabinder.spotiflyer.R -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.io.File -import java.io.IOException - -fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{ - return Environment.getExternalStorageDirectory().toString() + File.separator + - SpotifyDownloadHelper.defaultDir + SpotifyDownloadHelper.removeIllegalChars(type) + File.separator + - (if(subFolder == null){""}else{ SpotifyDownloadHelper.removeIllegalChars(subFolder) + File.separator} - + itemName?.let { SpotifyDownloadHelper.removeIllegalChars(it) + extension}) -} - -fun rotateAnim(view: View){ - val rotate = RotateAnimation( - 0F, 360F, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f - ) - rotate.duration = 1000 - rotate.repeatCount = Animation.INFINITE - rotate.repeatMode = Animation.INFINITE - rotate.interpolator = LinearInterpolator() - view.animation = rotate -} - - -@BindingAdapter("imageUrl") -fun bindImage(imgView: ImageView, imgUrl: String?) { - imgUrl?.let { - val imgUri = imgUrl.toUri().buildUpon().scheme("https").build() - Glide - .with(imgView) - .asFile() - .load(imgUri) - .placeholder(R.drawable.ic_song_placeholder) - .error(R.drawable.ic_musicplaceholder) - .listener(object:RequestListener{ - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - Log.i("Glide","LoadFailed") - return false - } - - override fun onResourceReady( - resource: File?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean - ): Boolean { - CoroutineScope(Dispatchers.Main).launch { - try { - val file = File( - Environment.getExternalStorageDirectory(), - SpotifyDownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg" - ) // 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() - } - } - return false - } - }).submit() - } - } - -/** - *Extension Function For Copying Files! - **/ -fun File.copyTo(file: File) { - inputStream().use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } -} -fun createDirectory(dir:String){ - val yourAppDir = File(Environment.getExternalStorageDirectory(), - dir) - - if(!yourAppDir.exists() && !yourAppDir.isDirectory) - { // create empty directory - if (yourAppDir.mkdirs()) - {Log.i("CreateDir","App dir created")} - else - {Log.w("CreateDir","Unable to create app dir!")} - } - else - {Log.i("CreateDir","App dir already exists")} -} diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt new file mode 100644 index 00000000..896657ca --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Extensions.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.utils + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.view.View +import com.shabinder.spotiflyer.utils.Provider.mainActivity + +fun View.openPlatformOnClick(packageName:String, websiteAddress:String){ + val manager: PackageManager = mainActivity.packageManager + try { + val i = manager.getLaunchIntentForPackage(packageName) + ?: throw PackageManager.NameNotFoundException() + i.addCategory(Intent.CATEGORY_LAUNCHER) + this.setOnClickListener { mainActivity.startActivity(i) } + } catch (e: PackageManager.NameNotFoundException) { + val uri: Uri = + Uri.parse(websiteAddress) + val intent = Intent(Intent.ACTION_VIEW, uri) + this.setOnClickListener { mainActivity.startActivity(intent) } + } +} +fun View.openPlatformOnClick(websiteAddress:String){ + val uri: Uri = + Uri.parse(websiteAddress) + val intent = Intent(Intent.ACTION_VIEW, uri) + this.setOnClickListener { mainActivity.startActivity(intent) } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt new file mode 100644 index 00000000..b137c203 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/NetworkInterceptor.kt @@ -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 . + */ + +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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt index 57726455..033e5515 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Provider.kt @@ -18,11 +18,15 @@ package com.shabinder.spotiflyer.utils import android.content.Context +import android.os.Environment 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 @@ -36,23 +40,35 @@ import okhttp3.OkHttpClient import okhttp3.Request import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import java.io.File import javax.inject.Singleton + @InstallIn(ApplicationComponent::class) @Module object Provider { + val mainActivity: MainActivity = MainActivity.getInstance() + val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + + @Provides fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{ return DownloadRecordDatabase.getInstance(appContext).databaseDAO } + @Provides + @Singleton + fun getYTDownloader():YoutubeDownloader{ + return YoutubeDownloader() + } @Provides @Singleton fun provideUpi():EasyUpiPayment { - return EasyUpiPayment.Builder(MainActivity.getInstance()) + return EasyUpiPayment.Builder(mainActivity) .setPayeeVpa("technoshab@paytm") .setPayeeName("Shabinder Singh") .setTransactionId("UNIQUE_TRANSACTION_ID") @@ -72,29 +88,59 @@ object Provider { @Provides @Singleton - fun getYTDownloader():YoutubeDownloader{ - return YoutubeDownloader() - } - - @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 okHttpClient():OkHttpClient{ + return OkHttpClient.Builder() + .addInterceptor(NetworkInterceptor()) + .build() + } + + @Provides + @Singleton + fun getGaanaInterface(moshi: Moshi,okHttpClient: OkHttpClient):GaanaInterface{ + val retrofit = Retrofit.Builder() + .baseUrl("https://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(MoshiConverterFactory.create(moshi)) + .build() + return retrofit.create(YoutubeMusicApi::class.java) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt new file mode 100644 index 00000000..a5080d81 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListFragment.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.utils + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavArgs +import com.shabinder.spotiflyer.R +import com.shabinder.spotiflyer.SharedViewModel +import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding +import com.shabinder.spotiflyer.models.DownloadStatus +import com.shabinder.spotiflyer.models.TrackDetails +import com.shabinder.spotiflyer.models.spotify.Source +import com.shabinder.spotiflyer.recyclerView.TrackListAdapter +import com.shabinder.spotiflyer.utils.Provider.mainActivity + +abstract class TrackListFragment : Fragment() { + + protected lateinit var sharedViewModel: SharedViewModel + protected lateinit var binding: TrackListFragmentBinding + protected abstract var viewModel: VM + protected abstract var adapter: TrackListAdapter + protected abstract var source: Source + private var intentFilter: IntentFilter? = null + private var updateUIReceiver: BroadcastReceiver? = null + protected abstract val args:NavArgs + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if(!isOnline()){ + showNoConnectionAlert() + mainActivity.onBackPressed() + } + sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = TrackListFragmentBinding.inflate(inflater,container,false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initializeLiveDataObservers() + } + + /** + *Live Data Observers + **/ + private fun initializeLiveDataObservers() { + viewModel.trackList.observe(viewLifecycleOwner, { + if (!it.isNullOrEmpty()){ + Log.i("GaanaFragment","TrackList Updated") + adapter.submitList(it, source) + checkIfAllDownloaded() + } + }) + + viewModel.coverUrl.observe(viewLifecycleOwner, { + it?.let{bindImage(binding.coverImage,it, source)} + }) + + viewModel.title.observe(viewLifecycleOwner, { + binding.titleView.text = it + }) + } + + private fun initializeBroadcast() { + intentFilter = IntentFilter() + intentFilter?.addAction("track_download_completed") + + updateUIReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + //UI update here + if (intent != null){ + val trackDetails = intent.getParcelableExtra("track") + trackDetails?.let { + val position: Int = 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) + track?.let{ + it.downloaded = DownloadStatus.Downloaded + viewModel.trackList.value?.set(position, it) + adapter.notifyItemChanged(position) + checkIfAllDownloaded() + } + } + } + } + } + } + requireActivity().registerReceiver(updateUIReceiver, intentFilter) + } + + override fun onResume() { + super.onResume() + initializeBroadcast() + } + + override fun onPause() { + super.onPause() + requireActivity().unregisterReceiver(updateUIReceiver) + } + + private fun checkIfAllDownloaded() { + if(!viewModel.trackList.value!!.any { it.downloaded != DownloadStatus.Downloaded }){ + //All Tracks Downloaded + binding.btnDownloadAll.visibility = View.GONE + binding.downloadingFab.apply{ + setImageResource(R.drawable.ic_tick) + visibility = View.VISIBLE + clearAnimation() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt new file mode 100644 index 00000000..4bc69340 --- /dev/null +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/TrackListViewModel.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Shabinder Singh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.shabinder.spotiflyer.utils + +import 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 TrackListViewModel:ViewModel() { + abstract var folderType:String + abstract var subFolder:String + open val trackList = MutableLiveData>() + + private val viewModelJob:CompletableJob = Job() + open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) + + private val loading = "Loading!" + open var title = MutableLiveData().apply { value = loading } + open var coverUrl = MutableLiveData() + + override fun onCleared() { + super.onCleared() + viewModelJob.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt old mode 100644 new mode 100755 index 79f44b5a..ca125bd9 --- a/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/utils/Utils.kt @@ -17,16 +17,288 @@ package com.shabinder.spotiflyer.utils -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper +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 +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.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.spotify.Source +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 java.io.File +import java.io.IOException + +fun loadAllImages(context: Context?, images:ArrayList? = null ) { + val serviceIntent = Intent(context, ForegroundService::class.java) + images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) } + context?.let { ContextCompat.startForegroundService(it, serviceIntent) } +} + +fun startService(context:Context?,objects:ArrayList? = null ) { + val serviceIntent = Intent(context, ForegroundService::class.java) + objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) } + context?.let { ContextCompat.startForegroundService(it, serviceIntent) } +} + +fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{ + return Environment.getExternalStorageDirectory().toString() + File.separator + + defaultDir + removeIllegalChars(type) + File.separator + + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + + itemName?.let { removeIllegalChars(it) + extension}) +} + +/** + * Util. Function To Check Connection Status + **/ +@Suppress("DEPRECATION") +fun isOnline(): Boolean { + var result = false + val 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 { + result = when { + hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false + } + } + } else { + val netInfo = + (mainActivity.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo + result = netInfo != null && netInfo.isConnected + } + } + return result +} + +fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false , isError:Boolean = false){ + CoroutineScope(Dispatchers.Main).launch{ + Snackbar.make( + mainActivity.snackBarAnchor, + message, + if (long) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT + ).apply { + setAction("Ok") { + dismiss() + } + setActionTextColor(ContextCompat.getColor(mainActivity,R.color.black)) + when{ + isSuccess -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.successGreen)) + isError -> setBackgroundTint(ContextCompat.getColor(mainActivity,R.color.errorRed)) + } + }.show() + } +} + + +fun rotateAnim(view: View){ + val rotate = RotateAnimation( + 0F, 360F, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f + ) + rotate.duration = 1000 + rotate.repeatCount = Animation.INFINITE + rotate.repeatMode = Animation.INFINITE + rotate.interpolator = LinearInterpolator() + view.animation = rotate +} + +fun showNoConnectionAlert(){ + CoroutineScope(Dispatchers.Main).launch { + mainActivity.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() + Glide + .with(imgView) + .asFile() + .load(imgUri) + .placeholder(R.drawable.ic_song_placeholder) + .error(R.drawable.ic_musicplaceholder) + .listener(object:RequestListener{ + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + CoroutineScope(Dispatchers.Main).launch { + try { + val file = when(source){ + Source.Spotify->{ + File( + Environment.getExternalStorageDirectory(), + defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg" + ) + } + Source.YouTube->{ + //Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" + // We Are Naming using "$searchId" + File( + Environment.getExternalStorageDirectory(), + defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg" + ) + } + Source.Gaana -> { + File( + Environment.getExternalStorageDirectory(), + Provider.defaultDir +".Images/" + (imgUrl.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg") + } + else -> File( + Environment.getExternalStorageDirectory(), + defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg" + ) + } + // the File to save , append increasing numeric counter to prevent files from getting overwritten. + resource?.copyTo(file) + Glide.with(imgView) + .load(file) + .placeholder(R.drawable.ic_song_placeholder) + .into(imgView) + } catch (e: IOException) { + e.printStackTrace() + } + } + return false + } + }).submit() + } + } + +/** + *Extension Function For Copying Files! + **/ +fun File.copyTo(file: File) { + inputStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } +} +fun createDirectory(dir:String){ + val yourAppDir = File(Environment.getExternalStorageDirectory(), + dir) + + if(!yourAppDir.exists() && !yourAppDir.isDirectory) + { // create empty directory + if (yourAppDir.mkdirs()) + {Log.i("CreateDir","App dir created")} + else + {Log.w("CreateDir","Unable to create app dir!")} + } + else + {Log.i("CreateDir","App dir already exists")} +} +/** + * Removing Illegal Chars from File Name + * **/ +fun removeIllegalChars(fileName: String): String? { + val illegalCharArray = charArrayOf( + '/', + '\n', + '\r', + '\t', + '\u0000', + '\u000C', + '`', + '?', + '*', + '\\', + '<', + '>', + '|', + '\"', + '.', + '-', + '\'' + ) + + var name = fileName + for (c in illegalCharArray) { + name = fileName.replace(c, '_') + } + name = name.replace("\\s".toRegex(), "_") + name = name.replace("\\)".toRegex(), "") + name = name.replace("\\(".toRegex(), "") + name = name.replace("\\[".toRegex(), "") + name = name.replace("]".toRegex(), "") + name = name.replace("\\.".toRegex(), "") + name = name.replace("\"".toRegex(), "") + name = name.replace("\'".toRegex(), "") + name = name.replace(":".toRegex(), "") + name = name.replace("\\|".toRegex(), "") + return name +} fun createDirectories() { - createDirectory(SpotifyDownloadHelper.defaultDir) - createDirectory(SpotifyDownloadHelper.defaultDir + ".Images/") - createDirectory(SpotifyDownloadHelper.defaultDir + "Tracks/") - createDirectory(SpotifyDownloadHelper.defaultDir + "Albums/") - createDirectory(SpotifyDownloadHelper.defaultDir + "Playlists/") - createDirectory(SpotifyDownloadHelper.defaultDir + "YT_Downloads/") + createDirectory(defaultDir) + createDirectory(defaultDir + ".Images/") + createDirectory(defaultDir + "Tracks/") + createDirectory(defaultDir + "Albums/") + createDirectory(defaultDir + "Playlists/") + createDirectory(defaultDir + "YT_Downloads/") } fun getEmojiByUnicode(unicode: Int): String? { return String(Character.toChars(unicode)) -} \ No newline at end of file +} + +/* +internal val nullOnEmptyConverterFactory = object : Converter.Factory() { + fun converterFactory() = this + override fun responseBodyConverter( + type: Type, + annotations: Array, + retrofit: Retrofit + ) = object : Converter { + val nextResponseBodyConverter = + retrofit.nextResponseBodyConverter(converterFactory(), type, annotations) + + override fun convert(value: ResponseBody) = + if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null + } +}*/ diff --git a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt index a1922fa2..f6829393 100755 --- a/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt +++ b/app/src/main/java/com/shabinder/spotiflyer/worker/ForegroundService.kt @@ -17,6 +17,7 @@ package com.shabinder.spotiflyer.worker +import android.annotation.SuppressLint import android.app.* import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED import android.content.BroadcastReceiver @@ -28,27 +29,37 @@ import android.os.* import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.core.net.toUri import com.arthenica.mobileffmpeg.Config import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS import com.arthenica.mobileffmpeg.FFmpeg +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.github.kiulian.downloader.YoutubeDownloader +import com.github.kiulian.downloader.model.formats.Format +import com.github.kiulian.downloader.model.quality.AudioQuality import com.mpatric.mp3agic.ID3v1Tag import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.Mp3File import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.R -import com.shabinder.spotiflyer.downloadHelper.SpotifyDownloadHelper import com.shabinder.spotiflyer.models.DownloadObject -import com.shabinder.spotiflyer.models.Track +import com.shabinder.spotiflyer.models.TrackDetails +import com.shabinder.spotiflyer.utils.Provider +import com.shabinder.spotiflyer.utils.copyTo import com.tonyodev.fetch2.* import com.tonyodev.fetch2core.DownloadBlock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.io.File import java.io.FileInputStream +import java.io.IOException +import java.util.* +@Suppress("DEPRECATION") class ForegroundService : Service(){ private val tag = "Foreground Service" private val channelId = "ForegroundDownloaderService" @@ -56,22 +67,21 @@ class ForegroundService : Service(){ private var total = 0 //Total Downloads Requested private var converted = 0//Total Files Converted private var downloaded = 0//Total Files downloaded - private var fetch:Fetch? = null - private var downloadManager : DownloadManager? = null - private var downloadList = mutableListOf() + private lateinit var fetch:Fetch + private lateinit var ytDownloader: YoutubeDownloader + private lateinit var downloadManager : DownloadManager private var serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) - private val requestMap = mutableMapOf() - private val downloadMap = mutableMapOf() + private val requestMap = mutableMapOf() private var speed :Long = 0 - private var defaultDirectory = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + private var defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator private val parentDirectory = File(Environment.getExternalStorageDirectory(), - defaultDirectory+File.separator + defaultDir +File.separator ) private var wakeLock: PowerManager.WakeLock? = null private var isServiceStarted = false var notificationLine = 0 - val messageList = mutableListOf("","","","") + val messageList = mutableListOf("","","","") private var pendingIntent:PendingIntent? = null @@ -88,7 +98,7 @@ class ForegroundService : Service(){ 0, notificationIntent, 0 ) downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - + ytDownloader = YoutubeDownloader() val fetchConfiguration = FetchConfiguration.Builder(this) .setDownloadConcurrentLimit(4) @@ -97,90 +107,41 @@ class ForegroundService : Service(){ Fetch.setDefaultInstanceConfiguration(fetchConfiguration) fetch = Fetch.getDefaultInstance() -// fetch?.enableLogging(true) - fetch?.addListener(fetchListener) + fetch.addListener(fetchListener) //clearing all not completed Downloads //Starting fresh - fetch?.removeAll() + fetch.removeAll() startForeground() } - /** - *Starting Service with Notification as Foreground! - **/ - private fun startForeground() { - val channelId = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(channelId, "Downloader Service") - } else { - // If earlier version channel ID is not used - // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) - "" - } - - val notification = NotificationCompat.Builder(this, channelId) - .setSmallIcon(R.drawable.down_arrowbw) - .setNotificationSilent() - .setSubText("Total: $total Completed:$converted") - .setStyle(NotificationCompat.InboxStyle() - .setBigContentTitle("Speed: $speed KB/s") - .addLine(messageList[0]) - .addLine(messageList[1]) - .addLine(messageList[2]) - .addLine(messageList[3])) - .setContentIntent(pendingIntent) - .build() - startForeground(notificationId, notification) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun createNotificationChannel(channelId: String, channelName: String): String{ - val chan = NotificationChannel(channelId, - channelName, NotificationManager.IMPORTANCE_DEFAULT) - chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - service.createNotificationChannel(chan) - return channelId - } - + @SuppressLint("WakelockTimeout") override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { // Send a notification that service is started Log.i(tag,"Service Started.") startForeground() - //do heavy work on a background thread - //val list = intent.getSerializableExtra("list") as List -// val list = intent.getParcelableArrayListExtra("list") ?: intent.extras?.getParcelableArrayList("list") -// Log.i(tag,"Intent List Size: ${list!!.size}") - val obj = intent.getParcelableExtra("object") ?: intent.extras?.getParcelable("object") - obj?.let { - total ++ -// Log.i(tag,"Intent List Size: ${list!!.size}") - updateNotification() - serviceScope.launch { - val request= Request(obj.url, obj.outputDir) - request.priority = Priority.NORMAL - request.networkType = NetworkType.ALL + val downloadObjects: ArrayList? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object")) + val imagesList: ArrayList? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList")) - fetch!!.enqueue(request, - { - obj.track?.let { it1 -> requestMap.put(it, it1) } - downloadList.remove(obj) - Log.i(tag, "Enqueuing Download") - }, - { - Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")} - ) + imagesList?.let{ + serviceScope.launch { + loadAllImages(it) } } + downloadObjects?.let { + total += downloadObjects.size + updateNotification() + downloadAllTracks(downloadObjects) + } + //Wake locks and misc tasks from here : return if (isServiceStarted){ + //Service Already Started START_STICKY } else{ Log.i(tag,"Starting the foreground service task") isServiceStarted = true - wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply { @@ -191,9 +152,53 @@ class ForegroundService : Service(){ } } + private fun downloadAllTracks(downloadObjects: List){ + serviceScope.launch(Dispatchers.IO) { + for(downloadObj in downloadObjects){ + try { + val video = ytDownloader.getVideo(downloadObj.ytVideoId) + val format: Format? = try { + video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format + } catch (e: java.lang.IndexOutOfBoundsException) { + try { + video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format + } catch (e: java.lang.IndexOutOfBoundsException) { + try { + video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format + } catch (e: java.lang.IndexOutOfBoundsException) { + Log.i("YTDownloader", e.toString()) + null + } + } + } + format?.let { + val url: String = format.url() + Log.i("DHelper Link Found", url) + serviceScope.launch { + val request= Request(url, downloadObj.outputFile) + request.priority = Priority.NORMAL + request.networkType = NetworkType.ALL + + fetch.enqueue(request, + { + requestMap[it] = downloadObj.trackDetails + Log.i(tag, "Enqueuing Download") + }, + { + Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")} + ) + } + } + }catch (e: com.github.kiulian.downloader.YoutubeException){ + Log.i("Service YT Error", e.message.toString()) + } + } + } + } + override fun onDestroy() { super.onDestroy() - if(downloadMap.isEmpty() && converted == total){ + if(converted == total){ Handler().postDelayed({ Log.i(tag,"Service destroyed.") deleteFile(parentDirectory) @@ -203,25 +208,11 @@ class ForegroundService : Service(){ } } - private fun releaseWakeLock() { - Log.i(tag,"Releasing Wake Lock") - try { - wakeLock?.let { - if (it.isHeld) { - it.release() - } - } - } catch (e: Exception) { - Log.i(tag,"Service stopped without being started: ${e.message}") - } - isServiceStarted = false - } - - override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) - if(downloadMap.isEmpty() && converted == total ){ + if(converted == total ){ Log.i(tag,"Service Removed.") + deleteFile(parentDirectory) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { stopForeground(true) } else { @@ -230,25 +221,6 @@ class ForegroundService : Service(){ } } - /** - * Deleting All Residual Files except Mp3 Files - **/ - private fun deleteFile(dir:File) { - Log.i(tag,"Starting Deletions in ${dir.path} ") - val fList = dir.listFiles() - fList?.let { - for (file in fList) { - if (file.isDirectory) { - deleteFile(file) - } else if(file.isFile) { - if(file.path.toString().substringAfterLast(".") != "mp3"){ -// Log.i(tag,"deleting ${file.path}") - file.delete() - } - } - } - } - } /** * Fetch Listener/ Responsible for Fetch Behaviour @@ -277,23 +249,23 @@ class ForegroundService : Service(){ val track = requestMap[download.request] when(notificationLine){ 0 -> { - messageList[0] = "Downloading ${track?.name}" + messageList[0] = "Downloading ${track?.title}" notificationLine = 1 } 1 -> { - messageList[1] = "Downloading ${track?.name}" + messageList[1] = "Downloading ${track?.title}" notificationLine = 2 } 2-> { - messageList[2] = "Downloading ${track?.name}" + messageList[2] = "Downloading ${track?.title}" notificationLine = 3 } 3 -> { - messageList[3] = "Downloading ${track?.name}" + messageList[3] = "Downloading ${track?.title}" notificationLine = 0 } } - Log.i(tag,"${track?.name} Download Started") + Log.i(tag,"${track?.title} Download Started") updateNotification() } @@ -312,32 +284,27 @@ class ForegroundService : Service(){ override fun onCompleted(download: Download) { val track = requestMap[download.request] for (message in messageList){ - if( message == "Downloading ${track?.name}"){ + if( message == "Downloading ${track?.title}"){ + //Remove Downloading Status from Notification messageList[messageList.indexOf(message)] = "" } } - //Notify Download Completed - val intent = Intent() - .setAction("track_download_completed") - .putExtra("track",track) - this@ForegroundService.sendBroadcast(intent) - serviceScope.launch { try{ - convertToMp3(download.file, track!!) - Log.i(tag,"${track.name} Download Completed") + track?.let { convertToMp3(download.file, it) } + Log.i(tag,"${track?.title} Download Completed") }catch (e:KotlinNullPointerException ){ - Log.i(tag,"${track?.name} Download Failed! Error:Fetch!!!!") - Log.i(tag,"${track?.name} Requesting Download thru Android DM") + Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!") + Log.i(tag,"${track?.title} Requesting Download thru Android DM") downloadUsingDM(download.request.url,download.request.file, track!!) downloaded++ requestMap.remove(download.request) } } speed = 0 - updateNotification() +// updateNotification() } override fun onDeleted(download: Download) { @@ -357,7 +324,7 @@ class ForegroundService : Service(){ val track = requestMap[download.request] downloaded++ Log.i(tag,download.error.throwable.toString()) - Log.i(tag,"${track?.name} Requesting Download thru Android DM") + Log.i(tag,"${track?.title} Requesting Download thru Android DM") downloadUsingDM(download.request.url,download.request.file, track!!) requestMap.remove(download.request) } @@ -374,9 +341,9 @@ class ForegroundService : Service(){ downloadedBytesPerSecond: Long ) { val track = requestMap[download.request] - Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec") + Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec") speed = (downloadedBytesPerSecond/1000) - updateNotification() +// updateNotification() } } @@ -384,7 +351,7 @@ class ForegroundService : Service(){ /** * If fetch Fails , Android Download Manager To RESCUE!! **/ - fun downloadUsingDM(url:String, outputDir:String, track: Track){ + fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){ val uri = Uri.parse(url) val request = DownloadManager.Request(uri) .setAllowedNetworkTypes( @@ -392,14 +359,14 @@ class ForegroundService : Service(){ DownloadManager.Request.NETWORK_MOBILE ) .setAllowedOverRoaming(false) - .setTitle(track.name) + .setTitle(track.title) .setDescription("Spotify Downloader Working Up here...") .setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix( Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator )) .setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) //Start Download - val downloadID = downloadManager?.enqueue(request) + val downloadID = downloadManager.enqueue(request) Log.i("DownloadManager", "Download Request Sent") val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() { @@ -421,7 +388,7 @@ class ForegroundService : Service(){ /** *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata) **/ - fun convertToMp3(filePath: String, track: Track){ + fun convertToMp3(filePath: String, track: TrackDetails){ val m4aFile = File(filePath) FFmpeg.executeAsync( @@ -444,7 +411,7 @@ class ForegroundService : Service(){ } } - private fun writeMp3Tags(filePath:String, track: Track){ + private fun writeMp3Tags(filePath:String, track: TrackDetails){ var mp3File = Mp3File(filePath) mp3File = removeAllTags(mp3File) mp3File = setId3v1Tags(mp3File,track) @@ -457,11 +424,17 @@ class ForegroundService : Service(){ newFile.renameTo(file) converted++ updateNotification() + + //Notify Download Completed + val intent = Intent() + .setAction("track_download_completed") + .putExtra("track",track) + this@ForegroundService.sendBroadcast(intent) + //All tasks completed (REST IN PEACE) if(converted == total){ onDestroy() } - } /** @@ -475,7 +448,7 @@ class ForegroundService : Service(){ .setSubText("Total: $total Completed:$converted") .setNotificationSilent() .setStyle(NotificationCompat.InboxStyle() - .setBigContentTitle("Speed: $speed KB/s") +// .setBigContentTitle("Speed: $speed KB/s") .addLine(messageList[0]) .addLine(messageList[1]) .addLine(messageList[2]) @@ -486,64 +459,42 @@ class ForegroundService : Service(){ } /** - *Modifying Mp3 Tags with MetaData! + *Modifying Mp3 com.shabinder.spotiflyer.models.gaana.Tags with MetaData! **/ - private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File { - val id3v1Tag = ID3v1Tag() - id3v1Tag.track = track.disc_number.toString() - val artistsList = mutableListOf() - track.artists?.forEach { artistsList.add(it!!.name!!) } - id3v1Tag.artist = artistsList.joinToString() - id3v1Tag.title = track.name - id3v1Tag.album = track.album?.name - id3v1Tag.year = track.album?.release_date - id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}" + private fun setId3v1Tags(mp3File: Mp3File, track: TrackDetails): Mp3File { + val id3v1Tag = ID3v1Tag().apply { + artist = track.artists.joinToString(",") + title = track.title + album = track.albumName + year = track.year + comment = "Genres:${track.comment}" + } mp3File.id3v1Tag = id3v1Tag return mp3File } - private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File { - val id3v2Tag = ID3v24Tag() - id3v2Tag.track = track.disc_number.toString() - val artistsList = mutableListOf() - track.artists?.forEach { artistsList.add(it!!.name!!) } - id3v2Tag.artist = artistsList.joinToString() - id3v2Tag.title = track.name - id3v2Tag.album = track.album?.name - id3v2Tag.year = track.album?.release_date - id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}" - id3v2Tag.lyrics = "Gonna Implement Soon" - val copyrights = mutableListOf() - track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) } - id3v2Tag.copyright = copyrights.joinToString() - id3v2Tag.url = track.href - track.ytCoverUrl?.let { - val file = File( - Environment.getExternalStorageDirectory(), - SpotifyDownloadHelper.defaultDir +".Images/" + it.substringAfterLast('/',it) + ".jpeg") - Log.i("Mp3Tags editing Tags",file.path) - //init array with file length - val bytesArray = ByteArray(file.length().toInt()) - val fis = FileInputStream(file) + private fun setId3v2Tags(mp3file: Mp3File, track: TrackDetails): Mp3File { + val id3v2Tag = ID3v24Tag().apply { + artist = track.artists.joinToString(",") + title = track.title + album = track.albumName + year = track.year + comment = "Genres:${track.comment}" + lyrics = "Gonna Implement Soon" + url = track.trackUrl + } + val bytesArray = ByteArray(track.albumArt.length().toInt()) + try{ + val fis = FileInputStream(track.albumArt) fis.read(bytesArray) //read file into bytes[] fis.close() id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") + }catch (e:java.io.FileNotFoundException){ + Log.i("Error","Couldn't Write Mp3 Album Art") } - track.album?.let { - val file = File( - Environment.getExternalStorageDirectory(), - SpotifyDownloadHelper.defaultDir +".Images/" + (it.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg") - Log.i("Mp3Tags editing Tags",file.path) - //init array with file length - val bytesArray = ByteArray(file.length().toInt()) - val fis = FileInputStream(file) - fis.read(bytesArray) //read file into bytes[] - fis.close() - id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") - } - id3v2Tag.albumImage mp3file.id3v2Tag = id3v2Tag return mp3file } + private fun removeAllTags(mp3file: Mp3File): Mp3File { if (mp3file.hasId3v1Tag()) { mp3file.removeId3v1Tag() @@ -557,4 +508,146 @@ class ForegroundService : Service(){ return mp3file } + private fun releaseWakeLock() { + Log.i(tag,"Releasing Wake Lock") + try { + wakeLock?.let { + if (it.isHeld) { + it.release() + } + } + } catch (e: Exception) { + Log.i(tag,"Service stopped without being started: ${e.message}") + } + isServiceStarted = false + } + + /** + *Starting Service with Notification as Foreground! + **/ + private fun startForeground() { + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(channelId, "Downloader Service") + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val notification = NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.down_arrowbw) + .setNotificationSilent() + .setSubText("Total: $total Completed:$converted") + .setStyle(NotificationCompat.InboxStyle() +// .setBigContentTitle("Speed: $speed KB/s") + .addLine(messageList[0]) + .addLine(messageList[1]) + .addLine(messageList[2]) + .addLine(messageList[3])) + .setContentIntent(pendingIntent) + .build() + startForeground(notificationId, notification) + } + + @Suppress("SameParameterValue") + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_DEFAULT) + chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + service.createNotificationChannel(chan) + return channelId + } + + /** + * Deleting All Residual Files except Mp3 Files + **/ + private fun deleteFile(dir:File) { + Log.i(tag,"Starting Deletions in ${dir.path} ") + val fList = dir.listFiles() + fList?.let { + for (file in fList) { + if (file.isDirectory) { + deleteFile(file) + } else if(file.isFile) { + if(file.path.toString().substringAfterLast(".") != "mp3"){ + Log.i(tag,"deleting ${file.path}") + file.delete() + } + } + } + } + } + + /** + * Function to fetch all Images for use in mp3 tags. + **/ + private suspend fun loadAllImages(urlList: ArrayList) { + /* + * Last Element of this List defines Its Source + * */ + val source = urlList.last() + for (url in urlList.subList(0,urlList.size-2)) { + val imgUri = url.toUri().buildUpon().scheme("https").build() + Glide + .with(this) + .asFile() + .load(imgUri) + .listener(object: RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + Log.i("Glide","LoadFailed") + return false + } + + override fun onResourceReady( + resource: File?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + serviceScope.launch { + withContext(Dispatchers.IO){ + try { + val file = when(source){ + "spotify" ->{ + File( + Environment.getExternalStorageDirectory(), + defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg" + ) + } + "youtube" ->{ + File( + Environment.getExternalStorageDirectory(), + defaultDir +".Images/" + url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg" + ) + } + "gaana" -> { + File( + Environment.getExternalStorageDirectory(), + Provider.defaultDir +".Images/" + (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg") + } + else -> File( + Environment.getExternalStorageDirectory(), + defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg") + } + resource?.copyTo(file) + } catch (e: IOException) { + e.printStackTrace() + } + } + } + return false + } + }).submit() + } + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/gaana.png b/app/src/main/res/drawable/gaana.png new file mode 100644 index 00000000..fbf81daf Binary files /dev/null and b/app/src/main/res/drawable/gaana.png differ diff --git a/app/src/main/res/drawable/gradient.xml b/app/src/main/res/drawable/gradient.xml index 6177f9f4..b152852d 100755 --- a/app/src/main/res/drawable/gradient.xml +++ b/app/src/main/res/drawable/gradient.xml @@ -16,7 +16,8 @@ ~ along with this program. If not, see . --> - + + . + --> + + + + diff --git a/app/src/main/res/font/nunito_sans.ttf b/app/src/main/res/font/nunito_sans.ttf new file mode 100644 index 00000000..9abe932d Binary files /dev/null and b/app/src/main/res/font/nunito_sans.ttf differ diff --git a/app/src/main/res/font/nunito_sans_light.ttf b/app/src/main/res/font/nunito_sans_light.ttf new file mode 100644 index 00000000..2f5d0493 Binary files /dev/null and b/app/src/main/res/font/nunito_sans_light.ttf differ diff --git a/app/src/main/res/layout/download_record_fragment.xml b/app/src/main/res/layout/download_record_fragment.xml index 3e921168..29be0126 100755 --- a/app/src/main/res/layout/download_record_fragment.xml +++ b/app/src/main/res/layout/download_record_fragment.xml @@ -15,10 +15,9 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - - @@ -27,14 +26,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:background="@drawable/text_background_accented" + style="@style/Widget.AppCompat.TextView.Gradient" android:drawablePadding="5dp" android:fontFamily="@font/raleway_semibold" - android:gravity="center" - android:padding="8dp" android:text=" Download History " - android:textAlignment="center" - android:textColor="#E1FFFFFF" android:textSize="21sp" app:drawableStartCompat="@drawable/ic_history" app:layout_constraintEnd_toEndOf="parent" @@ -43,6 +38,7 @@ + - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/download_record_item.xml b/app/src/main/res/layout/download_record_item.xml index d301d54e..2324b2de 100755 --- a/app/src/main/res/layout/download_record_item.xml +++ b/app/src/main/res/layout/download_record_item.xml @@ -17,89 +17,81 @@ ~ along with this program. If not, see . --> - - - - + - + - + - - - + - + - + - - + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index c90179a2..29e8d02a 100755 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -16,43 +16,35 @@ ~ along with this program. If not, see . --> - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + - - + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/navigation" + tools:ignore="FragmentTagUsage" /> - - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index f3103f74..14b0320a 100755 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -15,46 +15,40 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + + app:layout_constraintTop_toBottomOf="@+id/linkSearch" + tools:ignore="UnusedAttribute" /> + android:contentDescription="Open Download History Button" + app:srcCompat="@drawable/ic_history"/> + app:layout_constraintTop_toBottomOf="@id/appLogo" + /> - - - + app:layout_constraintEnd_toStartOf="@+id/btn_Gaana" + android:padding="6dp" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="@+id/appSubTitle" + app:layout_constraintTop_toBottomOf="@+id/appSubTitle"/> + + @@ -170,15 +145,15 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginEnd="24dp" + android:gravity="center" android:text="Usage Instructions!" android:textAlignment="center" android:textColor="#D0838383" android:textSize="14sp" - android:gravity="center" - app:layout_constraintBottom_toBottomOf="@+id/developer_insta_spotify" + app:layout_constraintBottom_toBottomOf="@+id/btn_Insta" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/btn_linkedin" - app:layout_constraintTop_toTopOf="@+id/btn_github_spotify" /> + app:layout_constraintTop_toTopOf="@+id/btn_github" /> + android:contentDescription="Open Github App Button" + app:layout_constraintVertical_chainStyle="packed"/> + android:contentDescription="Open LinkedIN App Button" + app:layout_constraintTop_toBottomOf="@+id/btn_github"/> - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/spotify_fragment.xml b/app/src/main/res/layout/track_list_fragment.xml similarity index 84% rename from app/src/main/res/layout/spotify_fragment.xml rename to app/src/main/res/layout/track_list_fragment.xml index 2270feaf..dfdfc714 100755 --- a/app/src/main/res/layout/spotify_fragment.xml +++ b/app/src/main/res/layout/track_list_fragment.xml @@ -16,19 +16,20 @@ ~ along with this program. If not, see . --> - @@ -72,14 +72,14 @@ app:toolbarId="@+id/toolbar"> - - - - + app:layout_constraintTop_toBottomOf="@id/appbar" /> - + diff --git a/app/src/main/res/layout/track_list_item.xml b/app/src/main/res/layout/track_list_item.xml index a809c617..ba3a0c59 100755 --- a/app/src/main/res/layout/track_list_item.xml +++ b/app/src/main/res/layout/track_list_item.xml @@ -16,97 +16,84 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> + - - - - - - - + android:contentDescription="Track Image" + android:scaleType="centerInside" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/artist" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_song_placeholder" /> - + - - - + - + - + - - - - + diff --git a/app/src/main/res/layout/youtube_fragment.xml b/app/src/main/res/layout/youtube_fragment.xml deleted file mode 100755 index 90c375e5..00000000 --- a/app/src/main/res/layout/youtube_fragment.xml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/navigation.xml b/app/src/main/res/navigation/navigation.xml index a333ae27..c2c0d882 100755 --- a/app/src/main/res/navigation/navigation.xml +++ b/app/src/main/res/navigation/navigation.xml @@ -22,15 +22,6 @@ android:id="@+id/navigation" app:startDestination="@id/mainFragment"> - - - + - - - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2824d54a..bc99d4bf 100755 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -20,10 +20,12 @@ #FC5C7D #CE1CFF - #799BFF + #9AB3FF #FFFFFF #99FFFFFF #000000 - + #121212 + #59C351 + #FF9494 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f54381ef..fab32e6a 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,37 +19,89 @@ + + + + + diff --git a/app/src/main/res/xml/app_update.xml b/app/src/main/res/xml/app_update.xml index 9be85832..4417913b 100755 --- a/app/src/main/res/xml/app_update.xml +++ b/app/src/main/res/xml/app_update.xml @@ -18,8 +18,8 @@ - 1.5.1 - 7 - https://github.com/Shabinder/SpotiFlyer/releases/download/1.5/SpotiFlyer-v1.5.apk + 1.6 + 8 + https://github.com/Shabinder/SpotiFlyer/releases/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index d5e5db36..0c91bfef 100755 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { ext{ kotlin_version = "1.4.10" navigationVersion = '2.3.0' - ext.hilt_version = '2.28-alpha' + ext.hilt_version = '2.29.1-alpha' } repositories { google() @@ -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 }