Merge pull request #20 from Shabinder/develop

Merge Changes From Develop to Origin v1.7
This commit is contained in:
Shabinder Singh 2020-12-02 21:59:54 +05:30 committed by GitHub
commit 9d04e09d61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1575 additions and 1288 deletions

View File

@ -5,10 +5,12 @@
<w>amita</w> <w>amita</w>
<w>cardview</w> <w>cardview</w>
<w>cherrypick</w> <w>cherrypick</w>
<w>crashlytics</w>
<w>downloadrecord</w> <w>downloadrecord</w>
<w>emoji</w> <w>emoji</w>
<w>ffmpeg</w> <w>ffmpeg</w>
<w>flyer</w> <w>flyer</w>
<w>fmpeg</w>
<w>gaana</w> <w>gaana</w>
<w>gener</w> <w>gener</w>
<w>hqdefault</w> <w>hqdefault</w>

View File

@ -12,7 +12,6 @@
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/mobile-ffmpeg" />
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />

View File

@ -15,42 +15,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-android-extensions' id 'kotlin-android'
apply plugin: 'kotlin-kapt' id 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin" id 'kotlin-parcelize'
apply plugin: 'dagger.hilt.android.plugin' id 'androidx.navigation.safeargs.kotlin'
apply plugin: 'kotlinx-serialization' id 'dagger.hilt.android.plugin'
id 'kotlinx-serialization'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
}
android { android {
compileSdkVersion 29 compileSdkVersion 30
buildToolsVersion "30.0.2" buildToolsVersion "30.0.2"
buildFeatures{ buildFeatures{
//dataBinding = true
viewBinding = true viewBinding = true
} }
defaultConfig { defaultConfig {
applicationId 'com.shabinder.spotiflyer' applicationId 'com.shabinder.spotiflyer'
minSdkVersion 22 minSdkVersion 22
targetSdkVersion 29 targetSdkVersion 30
versionCode 8 versionCode 9
versionName "1.6" versionName "1.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/ASL2.0'
exclude("META-INF/*.kotlin_module")
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
@ -62,73 +55,101 @@ android {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
lintOptions { lintOptions {
abortOnError false abortOnError false
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/ASL2.0'
exclude("META-INF/*.kotlin_module")
}
ndkVersion '21.3.6528147'
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include:['*.jar']) //Android
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.10" //noinspection DifferentStdlibGradleVersion
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.webkit:webkit:1.3.0' implementation 'androidx.webkit:webkit:1.3.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1' implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.2.1' 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-core:1.4.2-native-mt'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
//FFmpeg
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
//Room: Local SQL-lite Database
implementation "androidx.room:room-runtime:2.2.5" 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" kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-ktx:2.2.5" implementation "androidx.room:room-ktx:2.2.5"
//Hilt: Dependency Injection
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02' implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02' kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
implementation project(path: ':mobile-ffmpeg') //Glide-Image Loading
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {transitive = true}
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") { kapt ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {transitive = true}
transitive = true
}
kapt ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
transitive = true
}
//HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//Json
implementation 'com.squareup.moshi:moshi:1.11.0' implementation 'com.squareup.moshi:moshi:1.11.0'
implementation 'com.squareup.moshi:moshi-kotlin: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-moshi:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0" implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.beust:klaxon:5.4' implementation 'com.beust:klaxon:5.4'
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
//Crashlytics & Analytics
implementation platform('com.google.firebase:firebase-bom:26.1.0')
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
//Extras
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.mpatric:mp3agic:0.9.1' implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0' implementation 'com.shreyaspatil:EasyUpiPayment:3.0.0'
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' implementation 'com.github.javiersantos:AppUpdater:2.7'
implementation 'com.github.lzyzsd:circleprogress:1.2.1'
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5"
implementation 'com.github.sealedtx:java-youtube-downloader:2.4.4'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
testImplementation 'junit:junit:4.13.1' testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

View File

@ -20,12 +20,22 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.shabinder.spotiflyer"> package="com.shabinder.spotiflyer">
<queries>
<package android:name="com.gaana" />
<package android:name="com.spotify.music" />
<package android:name="com.google.android.youtube" />
</queries>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_STORAGE_PERMISSION" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /><!--For UPI Apps-->
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
@ -38,9 +48,11 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:forceDarkAllowed="true" android:forceDarkAllowed="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true"
tools:ignore="AllowBackup"
tools:targetApi="q"> tools:targetApi="q">
<!--android:requestLegacyExternalStorage="true" For SDK 28-->
<activity android:name="com.shabinder.spotiflyer.MainActivity" <activity android:name="com.shabinder.spotiflyer.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTask">

View File

@ -31,6 +31,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import com.github.javiersantos.appupdater.AppUpdater import com.github.javiersantos.appupdater.AppUpdater
@ -38,9 +39,12 @@ import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.databinding.MainActivityBinding
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.NetworkInterceptor
import com.shabinder.spotiflyer.utils.createDirectories
import com.shabinder.spotiflyer.utils.showMessage
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -52,14 +56,15 @@ import javax.inject.Inject
/* /*
* This is App's God Activity * This is App's God Activity
* */ * */
@Suppress("DEPRECATION")
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(){ class MainActivity : AppCompatActivity(){
private var spotifyService : SpotifyService? = null private var spotifyService : SpotifyService? = null
val viewModelScope : CoroutineScope
get() = sharedViewModel.viewModelScope
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
lateinit var snackBarAnchor: View
private lateinit var sharedViewModel: SharedViewModel private lateinit var sharedViewModel: SharedViewModel
private lateinit var navController: NavController lateinit var snackBarAnchor: View
lateinit var navController: NavController
@Inject lateinit var moshi: Moshi @Inject lateinit var moshi: Moshi
@Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest @Inject lateinit var spotifyServiceTokenRequest: SpotifyServiceTokenRequest
@ -72,18 +77,15 @@ class MainActivity : AppCompatActivity(){
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
navController = findNavController(R.id.navHostFragment) navController = findNavController(R.id.navHostFragment)
snackBarAnchor = binding.snackBarPosition snackBarAnchor = binding.snackBarPosition
authenticateSpotify() authenticateSpotify()
}
override fun onStart() {
super.onStart()
requestPermission() requestPermission()
disableDozeMode() disableDozeMode()
checkIfLatestVersion() checkIfLatestVersion()
createDirectories() createDirectories()
Log.i("Connection Status", isOnline().toString())
//starting Notification and Downloader Service!
startService(this)
handleIntentFromExternalActivity() handleIntentFromExternalActivity()
} }
@ -151,9 +153,8 @@ class MainActivity : AppCompatActivity(){
sharedViewModel.spotifyService.value = spotifyService sharedViewModel.spotifyService.value = spotifyService
} }
fun authenticateSpotify() { fun authenticateSpotify() {
sharedViewModel.uiScope.launch { sharedViewModel.viewModelScope.launch {
Log.i("Spotify Authentication","Started") Log.i("Spotify Authentication","Started")
val token = spotifyServiceTokenRequest.getToken() val token = spotifyServiceTokenRequest.getToken()
token.value?.let { token.value?.let {
@ -207,7 +208,7 @@ class MainActivity : AppCompatActivity(){
companion object{ companion object{
private lateinit var instance: MainActivity private lateinit var instance: MainActivity
fun getInstance():MainActivity = instance fun getInstance():MainActivity = this.instance
} }
init { init {

View File

@ -17,22 +17,15 @@
package com.shabinder.spotiflyer package com.shabinder.spotiflyer
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import kotlinx.coroutines.CoroutineScope import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class SharedViewModel : ViewModel() { class SharedViewModel @ViewModelInject constructor(
val youtubeMusicApi: YoutubeMusicApi
) : ViewModel() {
var intentString = MutableLiveData<String>() var intentString = MutableLiveData<String>()
var spotifyService = MutableLiveData<SpotifyService>() var spotifyService = MutableLiveData<SpotifyService>()
private var viewModelJob = Job()
val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
} }

View File

@ -22,7 +22,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@Entity( @Entity(
@ -48,10 +48,4 @@ data class DownloadRecord(
@ColumnInfo(name = "totalFiles") @ColumnInfo(name = "totalFiles")
var totalFiles:Int = 1, var totalFiles:Int = 1,
@ColumnInfo(name = "downloaded")
var downloaded:Boolean=false,
@ColumnInfo(name = "directory")
var directory:String?=null
):Parcelable ):Parcelable

View File

@ -1,166 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.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<TrackDetails>) {
resetStatusBar()// For New Download Request's Status
val downloadList = ArrayList<DownloadObject>()
withContext(Dispatchers.Main){
total += trackList.size // Adding New Download List Count to StatusBar
trackList.forEachIndexed { index, it ->
if(!isOnline()){
showNoConnectionAlert()
return@withContext
}
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
processed++
if(index == (trackList.size-1)){//LastElement
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<String>{
override fun onResponse(call: Call<String>, response: Response<String>) {
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<String>, 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"
}
}

View File

@ -1,70 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.downloadHelper
import android.os.Environment
import android.util.Log
import android.widget.Toast
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider.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 {
suspend fun downloadYTTracks(
type:String,
subFolder: String?,
tracks:List<TrackDetails>,
){
val downloadList = ArrayList<DownloadObject>()
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(
trackDetails = it,
ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast("."),
outputFile = outputFile
)
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)
}
}
}

View File

@ -19,16 +19,9 @@ package com.shabinder.spotiflyer.models
import android.os.Parcelable import android.os.Parcelable
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
@Parcelize
data class DownloadObject(
var trackDetails: TrackDetails,
var ytVideoId:String,
var outputFile:String
):Parcelable
@Parcelize @Parcelize
data class TrackDetails( data class TrackDetails(
var title:String, var title:String,
@ -42,11 +35,17 @@ data class TrackDetails(
var albumArt: File, var albumArt: File,
var albumArtURL: String, var albumArtURL: String,
var source: Source, var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var progress: Int = 0,
var outputFile: String,
var videoID:String? = null
):Parcelable ):Parcelable
enum class DownloadStatus{ enum class DownloadStatus{
Downloaded, Downloaded,
Downloading, Downloading,
NotDownloaded Queued,
NotDownloaded,
Converting,
Failed
} }

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models package com.shabinder.spotiflyer.models
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class YoutubeTrack( data class YoutubeTrack(

View File

@ -18,7 +18,6 @@
package com.shabinder.spotiflyer.models.gaana package com.shabinder.spotiflyer.models.gaana
data class GaanaPlaylist ( data class GaanaPlaylist (
val tags : String?,
val modified_on : String, val modified_on : String,
val count : Int, val count : Int,
val created_on : String, val created_on : String,

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Album( data class Album(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Artist( data class Artist(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Copyright( data class Copyright(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Episodes( data class Episodes(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Followers( data class Followers(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Image( data class Image(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class LinkedTrack( data class LinkedTrack(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class PagingObjectPlaylistTrack( data class PagingObjectPlaylistTrack(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class PagingObjectTrack( data class PagingObjectTrack(

View File

@ -19,7 +19,7 @@ package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.Json import com.squareup.moshi.Json
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Playlist( data class Playlist(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class PlaylistTrack( data class PlaylistTrack(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Token( data class Token(

View File

@ -19,7 +19,7 @@ package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Track( data class Track(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class UserPrivate( data class UserPrivate(

View File

@ -18,7 +18,7 @@
package com.shabinder.spotiflyer.models.spotify package com.shabinder.spotiflyer.models.spotify
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class UserPublic( data class UserPublic(

View File

@ -19,8 +19,11 @@ package com.shabinder.spotiflyer.networking
import com.shabinder.spotiflyer.models.Optional import com.shabinder.spotiflyer.models.Optional
import com.shabinder.spotiflyer.models.gaana.* import com.shabinder.spotiflyer.models.gaana.*
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url
const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990" const val gaana_token = "b2e6d7fbc136547a940516e9b77e5990"
@ -98,4 +101,9 @@ interface GaanaInterface {
@Query("limit") limit: Int = 50 @Query("limit") limit: Int = 50
): Optional<GaanaArtistTracks> ): Optional<GaanaArtistTracks>
/*
* Dynamic Url Requests
* */
@GET
fun getResponse(@Url url:String): Call<ResponseBody>
} }

View File

@ -17,23 +17,24 @@
package com.shabinder.spotiflyer.recyclerView package com.shabinder.spotiflyer.recyclerView
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding 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.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()) { class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()){
var source:Source =Source.Spotify var source:Source =Source.Spotify
@ -46,52 +47,78 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
return ViewHolder(binding) return ViewHolder(binding)
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{ if(itemCount == 1){ holder.binding.imageUrl.visibility = View.GONE}else{
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
bindImage(holder.binding.imageUrl,item.albumArtURL, source) bindImage(holder.binding.imageUrl,item.albumArtURL, source)
} }
} }
when (item.downloaded) { when (item.downloaded) {
DownloadStatus.Downloaded -> { DownloadStatus.Downloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_tick) holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.clearAnimation() holder.binding.btnDownload.apply{
setImageResource(R.drawable.ic_tick)
clearAnimation()
visible()
}
}
DownloadStatus.Queued -> {
holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.apply{
setImageResource(R.drawable.ic_refresh)
rotate()
visible()
}
}
DownloadStatus.Failed -> {
holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.apply{
setImageResource(R.drawable.ic_error)
clearAnimation()
visible()
}
} }
DownloadStatus.Downloading -> { DownloadStatus.Downloading -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) holder.binding.btnDownload.invisible()
rotateAnim(holder.binding.btnDownload) holder.binding.btnDownloadProgress.apply {
progress = item.progress
bottomText = "Downloading"
visible()
}
}
DownloadStatus.Converting -> {
holder.binding.btnDownload.invisible()
holder.binding.btnDownloadProgress.apply {
visible()
progress = 100
bottomText = "Converting"
}
} }
DownloadStatus.NotDownloaded -> { DownloadStatus.NotDownloaded -> {
holder.binding.btnDownload.setImageResource(R.drawable.ic_arrow) holder.binding.btnDownloadProgress.invisible()
holder.binding.btnDownload.clearAnimation() holder.binding.btnDownload.apply{
holder.binding.btnDownload.setOnClickListener{ setImageResource(R.drawable.ic_arrow)
clearAnimation()
visible()
setOnClickListener{
if(!isOnline()){ if(!isOnline()){
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
showMessage("Processing!") showMessage("Processing!")
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh) item.downloaded = DownloadStatus.Queued
rotateAnim(it)
item.downloaded = DownloadStatus.Downloading
when(source){ when(source){
Source.YouTube -> { Source.YouTube -> {
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
YTDownloadHelper.downloadYTTracks( downloadTracks(arrayListOf(item))
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
} }
} }
else -> { else -> {
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
DownloadHelper.downloadAllTracks( downloadTracks(arrayListOf(item))
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
} }
} }
} }
@ -99,9 +126,10 @@ class TrackListAdapter(private val viewModel :TrackListViewModel): ListAdapter<T
} }
} }
} }
}
holder.binding.trackName.text = "${if(item.title.length > 17){"${item.title.subSequence(0,16)}..."}else{item.title}}" holder.binding.trackName.text = if(item.title.length > 20){"${item.title.subSequence(0,18)}..."}else{item.title}
holder.binding.artist.text = "${item.artists.get(0)}..." holder.binding.artist.text = "${item.artists.firstOrNull()}..."
holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec" holder.binding.duration.text = "${item.durationSec/60} minutes, ${item.durationSec%60} sec"
} }

View File

@ -20,6 +20,7 @@ package com.shabinder.spotiflyer.splash
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
@ -33,7 +34,7 @@ class SplashScreen : AppCompatActivity(){
val splashTimeout = 400 val splashTimeout = 400
val homeIntent = Intent(this@SplashScreen, MainActivity::class.java) val homeIntent = Intent(this@SplashScreen, MainActivity::class.java)
Handler().postDelayed({ Handler(Looper.myLooper()!!).postDelayed({
//TODO:Bring Initial Setup here //TODO:Bring Initial Setup here
startActivity(homeIntent) startActivity(homeIntent)
finish() finish()

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.base
import android.content.Context
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.viewbinding.ViewBinding
import com.shabinder.spotiflyer.SharedViewModel
abstract class BaseFragment<VB:ViewBinding,VM : ViewModel> : Fragment() {
protected val sharedViewModel: SharedViewModel by activityViewModels()
protected abstract val binding: VB
protected abstract val viewModel: VM
protected val viewModelScope by lazy{viewModel.viewModelScope}
protected val applicationContext: Context
get() = requireActivity().applicationContext
}

View File

@ -0,0 +1,207 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.ui.base.tracklistbase
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.navigation.NavArgs
import com.shabinder.spotiflyer.R
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.ui.base.BaseFragment
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.tonyodev.fetch2.Status
abstract class TrackListFragment<VM : TrackListViewModel, args: NavArgs> : BaseFragment<TrackListFragmentBinding,VM>() {
override lateinit var binding: TrackListFragmentBinding
protected abstract var adapter: TrackListAdapter
protected abstract var source: Source
private var intentFilter: IntentFilter? = null
private var updateUIReceiver: BroadcastReceiver? = null
private var queryReceiver: BroadcastReceiver? = null
protected abstract val args:NavArgs
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(!isOnline()){
showNoConnectionAlert()
mainActivity.navController.popBackStack()
}
}
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)
binding.trackList.adapter = adapter
initializeLiveDataObservers()
}
/**
*Live Data Observers
**/
private fun initializeLiveDataObservers() {
viewModel.trackList.observe(viewLifecycleOwner, {
if (!it.isNullOrEmpty()){
Log.i("TrackListFragment","TrackList Updated")
adapter.submitList(it, source)
updateTracksStatus()
}
})
viewModel.coverUrl.observe(viewLifecycleOwner, {
it?.let{ bindImage(binding.coverImage,it, source) }
})
viewModel.title.observe(viewLifecycleOwner, {
binding.titleView.text = it
})
}
private fun initializeBroadcast() {
intentFilter = IntentFilter().apply {
addAction(Status.QUEUED.name)
addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction("Progress")
addAction("Converting")
addAction("track_download_completed")
}
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let {
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
Log.i("BroadCast Received","$position, ${intent.action} , ${trackDetails.title}")
if(position != -1) {
val track = viewModel.trackList.value?.get(position)
track?.let{
when(intent.action){
Status.QUEUED.name -> {
it.downloaded = DownloadStatus.Queued
}
Status.FAILED.name -> {
it.downloaded = DownloadStatus.Failed
}
Status.DOWNLOADING.name -> {
it.downloaded = DownloadStatus.Downloading
}
"Progress" -> {
//Progress Update
it.progress = intent.getIntExtra("progress",0)
it.downloaded = DownloadStatus.Downloading
}
"Converting" -> {
//Progress Update
it.downloaded = DownloadStatus.Converting
}
"track_download_completed" -> {
it.downloaded = DownloadStatus.Downloaded
}
}
viewModel.trackList.value?.set(position, it)
adapter.notifyItemChanged(position)
updateTracksStatus()
}
}
}
}
}
}
val queryFilter = IntentFilter().apply { addAction("query_result") }
queryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
val trackList = intent.getParcelableArrayListExtra<TrackDetails?>("tracks") ?: listOf()
Log.i("Service Response", "${trackList.size} Tracks Active")
for (trackDetails in trackList) {
trackDetails?.let { it ->
val position: Int = viewModel.trackList.value?.map { it.title }?.indexOf(trackDetails.title) ?: -1
Log.i("BroadCast Received","$position, ${it.downloaded} , ${trackDetails.title}")
if(position != -1) {
viewModel.trackList.value?.set(position,it)
adapter.notifyItemChanged(position)
updateTracksStatus()
}
}
}
}
}
}
requireActivity().registerReceiver(updateUIReceiver, intentFilter)
requireActivity().registerReceiver(queryReceiver, queryFilter)
}
override fun onResume() {
super.onResume()
initializeBroadcast()
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(updateUIReceiver)
requireActivity().unregisterReceiver(queryReceiver)
}
private fun updateTracksStatus() {
var allDownloaded = true
var allProcessing = true
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded)allDownloaded = false
if(track.downloaded == DownloadStatus.NotDownloaded)allProcessing = false
}
if(allProcessing){
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
setImageResource(R.drawable.ic_refresh)
visible()
rotate()
}
}
if(allDownloaded){
binding.btnDownloadAll.visibility = View.GONE
binding.downloadingFab.apply{
setImageResource(R.drawable.ic_tick)
visibility = View.VISIBLE
clearAnimation()
}
}
}
}

View File

@ -15,30 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.ui.base.tracklistbase
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.shabinder.spotiflyer.models.TrackDetails 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 class TrackListViewModel:ViewModel() {
abstract var folderType:String abstract var folderType:String
abstract var subFolder:String abstract var subFolder:String
open val trackList = MutableLiveData<MutableList<TrackDetails>>() open val trackList = MutableLiveData<MutableList<TrackDetails>>()
private val viewModelJob:CompletableJob = Job()
open val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val loading = "Loading!" private val loading = "Loading!"
open var title = MutableLiveData<String>().apply { value = loading } open var title = MutableLiveData<String>().apply { value = loading }
open var coverUrl = MutableLiveData<String>() open var coverUrl = MutableLiveData<String>()
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
} }

View File

@ -22,28 +22,23 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs 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.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source 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.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() { class GaanaFragment : TrackListFragment<GaanaViewModel, GaanaFragmentArgs>() {
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi override val viewModel: GaanaViewModel by viewModels()
@Inject lateinit var gaanaInterface: GaanaInterface
override lateinit var viewModel: GaanaViewModel
override lateinit var adapter: TrackListAdapter override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Gaana override var source: Source = Source.Gaana
override val args: GaanaFragmentArgs by navArgs() override val args: GaanaFragmentArgs by navArgs()
@ -51,10 +46,9 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
adapter = TrackListAdapter(viewModel)
initializeAll()
val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/") val gaanaLink = GaanaFragmentArgs.fromBundle(requireArguments()).link.substringAfter("gaana.com/")
//Link Schema: https://gaana.com/type/link //Link Schema: https://gaana.com/type/link
@ -77,56 +71,30 @@ class GaanaFragment : TrackListFragment<GaanaViewModel,GaanaFragmentArgs>() {
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply{
visible()
rotateAnim(binding.downloadingFab) rotate()
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Downloading
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
} }
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>() loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana)
viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("gaana")
loadAllImages(
requireActivity(),
urlList
)
} }
viewModel.uiScope.launch { viewModel.viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value?.filter{it.downloaded == DownloadStatus.NotDownloaded}
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( finalList?.let { it1 -> downloadTracks(it1 as ArrayList<TrackDetails>) }
viewModel.folderType, for (track in viewModel.trackList.value!!){
viewModel.subFolder, if(track.downloaded == DownloadStatus.NotDownloaded){
finalList ?: listOf(), track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
} }
} }
} }
} }
return binding.root 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
}
} }

View File

@ -17,44 +17,55 @@
package com.shabinder.spotiflyer.ui.gaana package com.shabinder.spotiflyer.ui.gaana
import android.os.Environment
import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.gaana.* import com.shabinder.spotiflyer.models.gaana.GaanaTrack
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.GaanaInterface import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.utils.Provider
import com.shabinder.spotiflyer.utils.TrackListViewModel
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){ class GaanaViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO,
private val gaanaInterface : GaanaInterface
) : TrackListViewModel(){
override var folderType:String = "" override var folderType:String = ""
override var subFolder:String = "" override var subFolder:String = ""
var gaanaInterface : GaanaInterface? = null
val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg" private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
fun gaanaSearch(type:String,link:String){ fun gaanaSearch(type:String,link:String){
when(type){ viewModelScope.launch {
when (type) {
"song" -> { "song" -> {
uiScope.launch { gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
getGaanaSong(link)?.tracks?.firstOrNull()?.also {
folderType = "Tracks" folderType = "Tracks"
if(File(finalOutputDir(it.track_title,folderType,subFolder)).exists()){//Download Already Present!! subFolder = ""
if (File(
finalOutputDir(
it.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
} }
trackList.value = listOf(it).toTrackDetailsList() trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.track_title title.value = it.track_title
coverUrl.value = it.artworkLink coverUrl.value = it.artworkLink
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
databaseDAO.insert( databaseDAO.insert(
DownloadRecord( DownloadRecord(
type = "Track", type = "Track",
@ -62,138 +73,132 @@ class GaanaViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO)
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value!!,
totalFiles = 1, totalFiles = 1,
downloaded = it.downloaded == DownloadStatus.Downloaded,
directory = finalOutputDir(it.track_title,folderType,subFolder)
) )
) )
} }
} }
} }
}
"album" -> { "album" -> {
uiScope.launch { gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
getGaanaAlbum(link)?.also {
folderType = "Albums" folderType = "Albums"
subFolder = link subFolder = link
it.tracks.forEach { track -> it.tracks.forEach { track ->
if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link title.value = link
coverUrl.value = it.custom_artworks.size_480p coverUrl.value = it.custom_artworks.size_480p
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
databaseDAO.insert(DownloadRecord( databaseDAO.insert(
DownloadRecord(
type = "Album", type = "Album",
name = title.value!!, name = title.value!!,
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, )
directory = finalOutputDir(type = folderType,subFolder = subFolder) )
))
}
} }
} }
} }
"playlist" -> { "playlist" -> {
uiScope.launch { gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
getGaanaPlaylist(link)?.also {
folderType = "Playlists" folderType = "Playlists"
subFolder = link subFolder = link
it.tracks.forEach {track -> it.tracks.forEach { track ->
if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link title.value = link
//coverUrl.value = "TODO" //coverUrl.value = "TODO"
coverUrl.value = gaanaPlaceholderImageUrl coverUrl.value = gaanaPlaceholderImageUrl
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
databaseDAO.insert(DownloadRecord( databaseDAO.insert(
DownloadRecord(
type = "Playlist", type = "Playlist",
name = title.value.toString(), name = title.value.toString(),
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = it.tracks.size, totalFiles = it.tracks.size,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, )
directory = finalOutputDir(type = folderType,subFolder = subFolder) )
))
}
} }
} }
} }
"artist" -> { "artist" -> {
uiScope.launch {
folderType = "Artist" folderType = "Artist"
subFolder = link subFolder = link
val artistDetails = getGaanaArtistDetails(link)?.artist?.firstOrNull()?.also { val artistDetails =
gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()
?.also {
title.value = it.name title.value = it.name
coverUrl.value = it.artworkLink coverUrl.value = it.artworkLink
} }
getGaanaArtistTracks(link)?.also { gaanaInterface.getGaanaArtistTracks(seokey = link).value?.also {
it.tracks.forEach {track -> it.tracks.forEach { track ->
if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!! if (File(
finalOutputDir(
track.track_title,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
databaseDAO.insert(DownloadRecord( databaseDAO.insert(
DownloadRecord(
type = "Artist", type = "Artist",
name = artistDetails?.name ?: link, name = artistDetails?.name ?: link,
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, )
directory = finalOutputDir(type = folderType,subFolder = subFolder) )
))
} }
} }
} }
} }
queryActiveTracks()
} }
} }
private fun List<GaanaTrack>.toTrackDetailsList(type:String , subFolder:String) = this.map {
private fun List<GaanaTrack>.toTrackDetailsList() = this.map {
TrackDetails( TrackDetails(
title = it.track_title, title = it.track_title,
artists = it.artist.map { artist -> artist?.name.toString() }, artists = it.artist.map { artist -> artist?.name.toString() },
durationSec = it.duration, durationSec = it.duration,
albumArt = File( albumArt = File(
Environment.getExternalStorageDirectory(), Provider.imageDir + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
Provider.defaultDir +".Images/" + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
albumName = it.album_title, albumName = it.album_title,
year = it.release_date, year = it.release_date,
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}", comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
trackUrl = it.lyrics_url, trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana, source = Source.Gaana,
albumArtURL = it.artworkLink albumArtURL = it.artworkLink,
outputFile = finalOutputDir(it.track_title,type, subFolder,".m4a")
) )
}.toMutableList() }.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
}
} }

View File

@ -23,7 +23,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.shabinder.spotiflyer.MainActivity import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
@ -33,6 +35,7 @@ import com.shabinder.spotiflyer.utils.*
import com.shreyaspatil.easyupipayment.EasyUpiPayment import com.shreyaspatil.easyupipayment.EasyUpiPayment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -40,16 +43,15 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainFragment : Fragment() { class MainFragment : Fragment() {
private lateinit var mainViewModel: MainViewModel private val mainViewModel: MainViewModel by viewModels()
private lateinit var sharedViewModel: SharedViewModel private val sharedViewModel: SharedViewModel by activityViewModels()
private lateinit var binding: MainFragmentBinding private lateinit var binding: MainFragmentBinding
@Inject lateinit var easyUpiPayment: EasyUpiPayment @Inject lateinit var easyUpiPayment: EasyUpiPayment
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
binding = MainFragmentBinding.inflate(inflater,container,false) binding = MainFragmentBinding.inflate(inflater,container,false)
initializeAll() initializeAll()
binding.btnSearch.setOnClickListener { binding.btnSearch.setOnClickListener {
@ -89,16 +91,16 @@ class MainFragment : Fragment() {
**/ **/
private fun handleIntent() { private fun handleIntent() {
sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let { sharedViewModel.intentString.observe(viewLifecycleOwner,{ it?.let {
sharedViewModel.uiScope.launch(Dispatchers.IO) { sharedViewModel.viewModelScope.launch(Dispatchers.IO) {
//Wait for any Authentication to Finish , //Wait for any Authentication to Finish ,
// this Wait prevents from multiple Authentication Requests // this Wait prevents from multiple Authentication Requests
Thread.sleep(1000) delay(1500)
if(sharedViewModel.spotifyService.value == null){ if(sharedViewModel.spotifyService.value == null){
//Not Authenticated Yet //Not Authenticated Yet
Provider.mainActivity.authenticateSpotify() Provider.mainActivity.authenticateSpotify()
while (sharedViewModel.spotifyService.value == null) { while (sharedViewModel.spotifyService.value == null) {
//Waiting for Authentication to Finish //Waiting for Authentication to Finish
Thread.sleep(1000) delay(1000)
} }
} }
@ -114,8 +116,6 @@ class MainFragment : Fragment() {
} }
private fun initializeAll() { private fun initializeAll() {
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
binding.apply { binding.apply {
btnGaana.openPlatformOnClick("com.gaana","http://gaana.com") btnGaana.openPlatformOnClick("com.gaana","http://gaana.com")
btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com") btnSpotify.openPlatformOnClick("com.spotify.music","http://open.spotify.com")
@ -139,4 +139,5 @@ class MainFragment : Fragment() {
.append(getText(R.string.d_three)).append("\n") .append(getText(R.string.d_three)).append("\n")
.append(getText(R.string.d_four)).append("\n") .append(getText(R.string.d_four)).append("\n")
} }
} }

View File

@ -23,29 +23,32 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.SimpleItemAnimator
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.YoutubeMusicApi import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.mainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>() { class SpotifyFragment : TrackListFragment<SpotifyViewModel, SpotifyFragmentArgs>() {
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi override val viewModel: SpotifyViewModel by viewModels()
override lateinit var viewModel: SpotifyViewModel override val args: SpotifyFragmentArgs by navArgs()
override lateinit var adapter: TrackListAdapter override lateinit var adapter: TrackListAdapter
override var source: Source = Source.Spotify override var source: Source = Source.Spotify
override val args: SpotifyFragmentArgs by navArgs() private val spotifyService:SpotifyService?
get() = sharedViewModel.spotifyService.value
lateinit var link:String
lateinit var type:String
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreateView( override fun onCreateView(
@ -55,71 +58,76 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
initializeAll() initializeAll()
val spotifyLink = args.link.substringAfter("open.spotify.com/") var spotifyLink = "https://" + args.link.substringAfterLast("https://").substringBefore(" ").trim()
Log.i("Spotify Fragment Link", spotifyLink)
viewModelScope.launch(Dispatchers.IO) {
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?') /*
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/') * New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_branch_match_id=862039436205270630
* */
if (!spotifyLink.contains("open.spotify")) {
val resolvedLink = viewModel.resolveLink(spotifyLink)
Log.d("Spotify Resolved Link", resolvedLink)
spotifyLink = resolvedLink
}
link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
Log.i("Spotify Fragment", "$type : $link") Log.i("Spotify Fragment", "$type : $link")
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
if(sharedViewModel.spotifyService.value == null){//Authentication pending!! if (isOnline()) mainActivity.authenticateSpotify()
if(isOnline()) mainActivity.authenticateSpotify()
} }
when{ when {
type == "Error" || link == "Error" -> { type == "Error" || link == "Error" -> {
showMessage("Please Check Your Link!") showMessage("Please Check Your Link!")
mainActivity.onBackPressed() mainActivity.onBackPressed()
} }
else -> { else -> {
if(type == "episode" || type == "show"){//TODO Implementation if (type == "episode" || type == "show") {//TODO Implementation
showMessage("Implementing Soon, Stay Tuned!") showMessage("Implementing Soon, Stay Tuned!")
} } else {
else{ viewModel.spotifySearch(type, link)
this.viewModel.spotifySearch(type,link)
binding.btnDownloadAll.setOnClickListener { binding.btnDownloadAll.setOnClickListener {
if(!isOnline()){ if (!isOnline()) {
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply {
visible()
rotateAnim(binding.downloadingFab) rotate()
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!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
val urlList = arrayListOf<String>()
this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
//Appending Source
urlList.add("spotify")
loadAllImages( loadAllImages(
requireActivity(), requireActivity(),
urlList viewModel.trackList.value?.map { it.albumArtURL },
Source.Spotify
) )
} }
this.viewModel.uiScope.launch { viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value?.filter{it.downloaded == DownloadStatus.NotDownloaded}
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( else downloadTracks(finalList as ArrayList<TrackDetails>)
viewModel.folderType, for (track in viewModel.trackList.value ?: listOf()) {
viewModel.subFolder, if (track.downloaded == DownloadStatus.NotDownloaded) {
finalList ?: listOf(), track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
}
} }
} }
} }
} }
} }
return binding.root return binding.root
} }
@ -127,15 +135,10 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel,SpotifyFragmentArgs>(
* Basic Initialization * Basic Initialization
**/ **/
private fun initializeAll() { private fun initializeAll() {
this.viewModel = ViewModelProvider(this).get(SpotifyViewModel::class.java)
adapter = TrackListAdapter(this.viewModel)
sharedViewModel.spotifyService.observe(viewLifecycleOwner, { sharedViewModel.spotifyService.observe(viewLifecycleOwner, {
this.viewModel.spotifyService = it this.viewModel.spotifyService = it
}) })
DownloadHelper.youtubeMusicApi = youtubeMusicApi viewModel.spotifyService = spotifyService //Temp Initialisation
DownloadHelper.sharedViewModel = sharedViewModel adapter = TrackListAdapter(this.viewModel)
DownloadHelper.statusBar = binding.statusBar
binding.trackList.adapter = adapter
(binding.trackList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
} }

View File

@ -17,95 +17,137 @@
package com.shabinder.spotiflyer.ui.spotify package com.shabinder.spotiflyer.ui.spotify
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.* import com.shabinder.spotiflyer.models.spotify.Album
import com.shabinder.spotiflyer.models.spotify.Image
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.models.spotify.Track
import com.shabinder.spotiflyer.networking.GaanaInterface
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.TrackListViewModel import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.finalOutputDir import com.shabinder.spotiflyer.utils.finalOutputDir
import com.shabinder.spotiflyer.utils.queryActiveTracks
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){ class SpotifyViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO,
val gaanaInterface : GaanaInterface
) : TrackListViewModel(){
override var folderType:String = "" override var folderType:String = ""
override var subFolder:String = "" override var subFolder:String = ""
var spotifyService : SpotifyService? = null var spotifyService : SpotifyService? = null
fun resolveLink(url:String):String {
val response = gaanaInterface.getResponse(url).execute().body()?.string().toString()
val regex = """https://open\.spotify\.com.+\w""".toRegex()
return regex.find(response)?.value.toString()
}
fun spotifySearch(type:String,link: String){ fun spotifySearch(type:String,link: String){
viewModelScope.launch {
when (type) { when (type) {
"track" -> { "track" -> {
uiScope.launch { spotifyService?.getTrack(link)?.value?.also {
getTrackDetails(link)?.also {
folderType = "Tracks" folderType = "Tracks"
if(File(finalOutputDir(it.name,folderType,subFolder)).exists()){//Download Already Present!! subFolder = ""
if (File(
finalOutputDir(
it.name.toString(),
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
} }
trackList.value = listOf(it).toTrackDetailsList() trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.name title.value = it.name
coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url
withContext(Dispatchers.IO){ ?: it.album!!.images?.elementAtOrNull(0)?.url
databaseDAO.insert(DownloadRecord( withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Track", type = "Track",
name = title.value!!, name = title.value!!,
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value!!,
totalFiles = 1, totalFiles = 1,
downloaded = it.downloaded == DownloadStatus.Downloaded, )
directory = finalOutputDir(it.name,folderType,subFolder) )
))
}
} }
} }
} }
"album" -> { "album" -> {
uiScope.launch { val albumObject = spotifyService?.getAlbum(link)?.value
val albumObject = getAlbumDetails(link)
folderType = "Albums" folderType = "Albums"
subFolder = albumObject?.name.toString() subFolder = albumObject?.name.toString()
albumObject?.tracks?.items?.forEach { albumObject?.tracks?.items?.forEach {
if(File(finalOutputDir(it.name!!,folderType,subFolder)).exists()){//Download Already Present!! if (File(
finalOutputDir(
it.name!!,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
} }
it.album = Album(images = listOf(Image(url = albumObject.images?.elementAtOrNull(1)?.url ?: albumObject.images?.elementAtOrNull(0)?.url ))) it.album = Album(
images = listOf(
Image(
url = albumObject.images?.elementAtOrNull(1)?.url
?: albumObject.images?.elementAtOrNull(0)?.url
)
)
)
} }
trackList.value = albumObject?.tracks?.items?.toTrackDetailsList() trackList.value = albumObject?.tracks?.items?.toTrackDetailsList(folderType, subFolder)
title.value = albumObject?.name title.value = albumObject?.name
coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url ?: albumObject?.images?.elementAtOrNull(0)?.url coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url
withContext(Dispatchers.IO){ ?: albumObject?.images?.elementAtOrNull(0)?.url
databaseDAO.insert(DownloadRecord( withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Album", type = "Album",
name = title.value!!, name = title.value!!,
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size, )
directory = finalOutputDir(type = folderType,subFolder = subFolder) )
))
}
} }
} }
"playlist" -> { "playlist" -> {
uiScope.launch { Log.i("Spotify Service",spotifyService.toString())
val playlistObject = getPlaylistDetails(link) val playlistObject = spotifyService?.getPlaylist(link)?.value
folderType = "Playlists" folderType = "Playlists"
subFolder = playlistObject?.name.toString() subFolder = playlistObject?.name.toString()
val tempTrackList = mutableListOf<Track>() val tempTrackList = mutableListOf<Track>()
Log.i("Tracks Fetched",playlistObject?.tracks?.items?.size.toString()) Log.i("Tracks Fetched", playlistObject?.tracks?.items?.size.toString())
playlistObject?.tracks?.items?.forEach { playlistObject?.tracks?.items?.forEach {
it.track?.let { it.track?.let { it1 ->
it1 -> if(File(finalOutputDir(it1.name!!,folderType,subFolder)).exists()){//Download Already Present!! if (File(
finalOutputDir(
it1.name!!,
folderType,
subFolder
)
).exists()
) {//Download Already Present!!
it1.downloaded = DownloadStatus.Downloaded it1.downloaded = DownloadStatus.Downloaded
} }
tempTrackList.add(it1) tempTrackList.add(it1)
@ -113,29 +155,29 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
} }
var moreTracksAvailable = !playlistObject?.tracks?.next.isNullOrBlank() var moreTracksAvailable = !playlistObject?.tracks?.next.isNullOrBlank()
while(moreTracksAvailable){ while (moreTracksAvailable) {
//Check For More Tracks If available //Check For More Tracks If available
val moreTracks = getPlaylistTrackDetails(link,offset = tempTrackList.size) val moreTracks = spotifyService?.getPlaylistTracks(link, offset = tempTrackList.size)?.value
moreTracks?.items?.forEach{ moreTracks?.items?.forEach {
it.track?.let { it1 -> tempTrackList.add(it1) } it.track?.let { it1 -> tempTrackList.add(it1) }
} }
moreTracksAvailable = !moreTracks?.next.isNullOrBlank() moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
} }
Log.i("Total Tracks Fetched",tempTrackList.size.toString()) Log.i("Total Tracks Fetched", tempTrackList.size.toString())
trackList.value = tempTrackList.toTrackDetailsList() trackList.value = tempTrackList.toTrackDetailsList(folderType, subFolder)
title.value = playlistObject?.name title.value = playlistObject?.name
coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url ?: playlistObject?.images?.firstOrNull()?.url.toString() coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url
withContext(Dispatchers.IO){ ?: playlistObject?.images?.firstOrNull()?.url.toString()
databaseDAO.insert(DownloadRecord( withContext(Dispatchers.IO) {
databaseDAO.insert(
DownloadRecord(
type = "Playlist", type = "Playlist",
name = title.value.toString(), name = title.value.toString(),
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = tempTrackList.size, totalFiles = tempTrackList.size,
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size, )
directory = finalOutputDir(type = folderType,subFolder = subFolder) )
))
}
} }
} }
"episode" -> {//TODO "episode" -> {//TODO
@ -143,41 +185,25 @@ class SpotifyViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
"show" -> {//TODO "show" -> {//TODO
} }
} }
queryActiveTracks()
}
} }
@Suppress("DEPRECATION") private fun List<Track>.toTrackDetailsList(type:String , subFolder:String) = this.map {
private fun List<Track>.toTrackDetailsList() = this.map {
TrackDetails( TrackDetails(
title = it.name.toString(), title = it.name.toString(),
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
durationSec = (it.duration_ms/1000).toInt(), durationSec = (it.duration_ms/1000).toInt(),
albumArt = File( albumArt = File(
Environment.getExternalStorageDirectory(), imageDir + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
Provider.defaultDir +".Images/" + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
albumName = it.album?.name, albumName = it.album?.name,
year = it.album?.release_date, year = it.album?.release_date,
comment = "Genres:${it.album?.genres?.joinToString()}", comment = "Genres:${it.album?.genres?.joinToString()}",
trackUrl = it.href, trackUrl = it.href,
downloaded = it.downloaded, downloaded = it.downloaded,
source = Source.Spotify, source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString() albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFile = finalOutputDir(it.name.toString(),type, subFolder,".m4a")
) )
}.toMutableList() }.toMutableList()
private suspend fun getTrackDetails(trackLink:String): Track?{
Log.i("Requesting","https://api.spotify.com/v1/tracks/$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)?.value
}
private suspend fun getPlaylistDetails(link:String): Playlist?{
Log.i("Requesting","https://api.spotify.com/v1/playlists/$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)?.value
}
} }

View File

@ -21,27 +21,26 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
private const val sampleDomain2 = "youtu.be" private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com" private const val sampleDomain1 = "youtube.com"
@AndroidEntryPoint @AndroidEntryPoint
class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>() { class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>(){
@Inject lateinit var ytDownloader: YoutubeDownloader override val viewModel: YoutubeViewModel by viewModels()
override lateinit var viewModel: YoutubeViewModel
override lateinit var adapter : TrackListAdapter override lateinit var adapter : TrackListAdapter
override var source: Source = Source.YouTube override var source: Source = Source.YouTube
override val args: YoutubeFragmentArgs by navArgs() override val args: YoutubeFragmentArgs by navArgs()
@ -49,11 +48,9 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
this.viewModel = ViewModelProvider(this).get(YoutubeViewModel::class.java) adapter = TrackListAdapter(viewModel)
adapter = TrackListAdapter(this.viewModel)
binding.trackList.adapter = adapter
val args = YoutubeFragmentArgs.fromBundle(requireArguments()) val args = YoutubeFragmentArgs.fromBundle(requireArguments())
val link = args.link val link = args.link
@ -66,17 +63,17 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
if(link.contains("playlist",true) || link.contains("list",true)){ if(link.contains("playlist",true) || link.contains("list",true)){
// Given Link is of a Playlist // Given Link is of a Playlist
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&") val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&")
this.viewModel.getYTPlaylist(playlistId,ytDownloader) viewModel.getYTPlaylist(playlistId)
}else{//Given Link is of a Video }else{//Given Link is of a Video
var searchId = "error" var searchId = "error"
if(link.contains(sampleDomain1,true) ){ if(link.contains(sampleDomain1,true) ){
searchId = link.substringAfterLast("=","error") searchId = link.substringAfterLast("=","error")
} }
if(link.contains(sampleDomain2,true) && !link.contains("playlist",true) ){ if(link.contains(sampleDomain2,true) ){
searchId = link.substringAfterLast("/","error") searchId = link.substringAfterLast("/","error")
} }
if(searchId != "error") { if(searchId != "error") {
this.viewModel.getYTTrack(searchId,ytDownloader) this.viewModel.getYTTrack(searchId)
}else{showMessage("Your Youtube Link is not of a Video!!")} }else{showMessage("Your Youtube Link is not of a Video!!")}
} }
@ -88,35 +85,24 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel,YoutubeFragmentArgs>(
showNoConnectionAlert() showNoConnectionAlert()
return@setOnClickListener return@setOnClickListener
} }
binding.btnDownloadAll.visibility = View.GONE binding.btnDownloadAll.gone()
binding.downloadingFab.visibility = View.VISIBLE binding.downloadingFab.apply{
visible()
rotateAnim(binding.downloadingFab) rotate()
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!") showMessage("Processing!")
sharedViewModel.uiScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
val urlList = arrayListOf<String>() loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube)
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 { viewModel.viewModelScope.launch {
YTDownloadHelper.downloadYTTracks( downloadTracks((viewModel.trackList.value?.filter { it.downloaded == DownloadStatus.NotDownloaded } ?: arrayListOf()) as ArrayList<TrackDetails>)
type = viewModel.folderType, for (track in viewModel.trackList.value?: listOf()){
subFolder = viewModel.subFolder, if(track.downloaded == DownloadStatus.NotDownloaded){
tracks = viewModel.trackList.value ?: listOf() track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
} }
} }
} }

View File

@ -18,23 +18,27 @@
package com.shabinder.spotiflyer.ui.youtube package com.shabinder.spotiflyer.ui.youtube
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.viewModelScope
import com.github.kiulian.downloader.YoutubeDownloader import com.github.kiulian.downloader.YoutubeDownloader
import com.shabinder.spotiflyer.database.DatabaseDAO import com.shabinder.spotiflyer.database.DatabaseDAO
import com.shabinder.spotiflyer.database.DownloadRecord import com.shabinder.spotiflyer.database.DownloadRecord
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.imageDir
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO) : TrackListViewModel(){ class YoutubeViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO,
private val ytDownloader: YoutubeDownloader
) : TrackListViewModel(){
/* /*
* YT Album Art Schema * YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" * HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
@ -44,10 +48,10 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
override var folderType = "YT_Downloads" override var folderType = "YT_Downloads"
override var subFolder = "" override var subFolder = ""
fun getYTPlaylist(searchId:String, ytDownloader:YoutubeDownloader){ fun getYTPlaylist(searchId:String){
if(!isOnline())return if(!isOnline())return
try{ try{
uiScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Playlist",searchId) Log.i("YT Playlist",searchId)
val playlist = ytDownloader.getPlaylist(searchId) val playlist = ytDownloader.getPlaylist(searchId)
val playlistDetails = playlist.details() val playlistDetails = playlist.details()
@ -64,8 +68,7 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
artists = listOf(it.author().toString()), artists = listOf(it.author().toString()),
durationSec = it.lengthSeconds(), durationSec = it.lengthSeconds(),
albumArt = File( albumArt = File(
Environment.getExternalStorageDirectory(), imageDir + it.videoId() + ".jpeg"
defaultDir + ".Images/" + it.videoId() + ".jpeg"
), ),
source = Source.YouTube, source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg", albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
@ -79,7 +82,9 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
DownloadStatus.Downloaded DownloadStatus.Downloaded
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
} },
outputFile = finalOutputDir(it.title(),folderType, subFolder,".m4a"),
videoID = it.videoId()
) )
}.toMutableList()) }.toMutableList())
@ -90,10 +95,9 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
link = "https://www.youtube.com/playlist?list=$searchId", link = "https://www.youtube.com/playlist?list=$searchId",
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg", coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg",
totalFiles = videos.size, totalFiles = videos.size,
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
)) ))
} }
queryActiveTracks()
} }
}catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){ }catch (e:com.github.kiulian.downloader.YoutubeException.BadPageException){
showMessage("An Error Occurred While Processing!") showMessage("An Error Occurred While Processing!")
@ -102,10 +106,10 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
fun getYTTrack(searchId:String, ytDownloader:YoutubeDownloader) { fun getYTTrack(searchId:String) {
if(!isOnline())return if(!isOnline())return
try{ try{
uiScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
Log.i("YT Video",searchId) Log.i("YT Video",searchId)
val video = ytDownloader.getVideo(searchId) val video = ytDownloader.getVideo(searchId)
coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg") coverUrl.postValue("https://i.ytimg.com/vi/$searchId/hqdefault.jpg")
@ -118,12 +122,22 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
title = name, title = name,
artists = listOf(detail?.author().toString()), artists = listOf(detail?.author().toString()),
durationSec = detail?.lengthSeconds()?:0, durationSec = detail?.lengthSeconds()?:0,
albumArt = File( albumArt = File(imageDir,"$searchId.jpeg"),
Environment.getExternalStorageDirectory(),
"$defaultDir.Images/$searchId.jpeg"
),
source = Source.YouTube, source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
downloaded = if (File(
finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder
)).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(name,folderType, subFolder,".m4a"),
videoID = searchId
) )
).toMutableList() ).toMutableList()
) )
@ -138,10 +152,9 @@ class YoutubeViewModel @ViewModelInject constructor(val databaseDAO: DatabaseDAO
link = "https://www.youtube.com/watch?v=$searchId", link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1, totalFiles = 1,
downloaded = false,
directory = finalOutputDir(type = "YT_Downloads")
)) ))
} }
queryActiveTracks()
} }
} catch (e:com.github.kiulian.downloader.YoutubeException){ } catch (e:com.github.kiulian.downloader.YoutubeException){
showMessage("An Error Occurred While Processing!") showMessage("An Error Occurred While Processing!")

View File

@ -21,6 +21,9 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import com.shabinder.spotiflyer.utils.Provider.mainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
fun View.openPlatformOnClick(packageName:String, websiteAddress:String){ fun View.openPlatformOnClick(packageName:String, websiteAddress:String){
@ -43,3 +46,25 @@ fun View.openPlatformOnClick(websiteAddress:String){
val intent = Intent(Intent.ACTION_VIEW, uri) val intent = Intent(Intent.ACTION_VIEW, uri)
this.setOnClickListener { mainActivity.startActivity(intent) } this.setOnClickListener { mainActivity.startActivity(intent) }
} }
fun View.rotate(){
val rotate = RotateAnimation(
0F, 360F,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f
)
rotate.duration = 2000
rotate.repeatCount = Animation.INFINITE
rotate.repeatMode = Animation.INFINITE
rotate.interpolator = LinearInterpolator()
this.animation = rotate
}
fun View.visible(){
this.visibility = View.VISIBLE
}
fun View.gone(){
this.visibility = View.GONE
}
fun View.invisible(){
this.visibility = View.INVISIBLE
}

View File

@ -17,9 +17,9 @@
package com.shabinder.spotiflyer.utils package com.shabinder.spotiflyer.utils
import android.util.Log
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Protocol import okhttp3.Protocol
import okhttp3.RequestBody
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.ResponseBody.Companion.toResponseBody
@ -27,13 +27,14 @@ const val NoInternetErrorCode = 222
class NetworkInterceptor: Interceptor { class NetworkInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
Log.i("Network Requesting",chain.request().url.toString())
return if (!isOnline()){ return if (!isOnline()){
//No Internet Connection //No Internet Connection
showNoConnectionAlert() showNoConnectionAlert()
//Lets Stop the Incoming Request //Lets Stop the Incoming Request
Response.Builder() Response.Builder()
.code(NoInternetErrorCode) // code(200.300) = successful else = unsuccessful .code(NoInternetErrorCode) // code(200.300) = successful else = unsuccessful
.body("{}".toResponseBody(null)) // Whatever body .body("{}".toResponseBody(null)) // Empty Object
.protocol(Protocol.HTTP_2) .protocol(Protocol.HTTP_2)
.message("No Internet Connection") .message("No Internet Connection")
.request(chain.request()) .request(chain.request())
@ -52,15 +53,7 @@ class NetworkInterceptor: Interceptor {
.message(response.message) .message(response.message)
.request(chain.request()) .request(chain.request())
.build() .build()
// chain.proceed(chain.request())
} }
} }
/*
* Converts REQUEST's Body to String
* */
private fun RequestBody?.bodyToString(): String {
if (this == null) return ""
val buffer = okio.Buffer()
writeTo(buffer)
return buffer.readUtf8()
}
} }

View File

@ -49,11 +49,24 @@ import javax.inject.Singleton
@Module @Module
object Provider { object Provider {
val mainActivity: MainActivity = MainActivity.getInstance() // mainActivity Instance to use whereEver Needed , as Its God Activity.
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator // (i.e, Active Throughout App' Lifecycle )
val mainActivity: MainActivity by lazy { MainActivity.getInstance() }
//Default Directory to save Media in their Own Categorized Folders
@Suppress("DEPRECATION")// We Do Have Media Access (But Just Media in Media Directory,Not Anything Else)
val defaultDir = Environment.getExternalStorageDirectory().toString() + File.separator +
Environment.DIRECTORY_MUSIC + File.separator +
"SpotiFlyer"+ File.separator
//Default Cache Directory to save Album Art to use them for writing in Media Later
val imageDir:String by lazy { mainActivity
.externalCacheDir?.absolutePath + File.separator +
".Images" + File.separator }
@Provides @Provides
@Singleton
fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{ fun databaseDAO(@ApplicationContext appContext: Context):DatabaseDAO{
return DownloadRecordDatabase.getInstance(appContext).databaseDAO return DownloadRecordDatabase.getInstance(appContext).databaseDAO
} }

View File

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

View File

@ -22,12 +22,7 @@ import android.content.Intent
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.Environment
import android.util.Log 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 android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
@ -39,9 +34,10 @@ import com.bumptech.glide.request.target.Target
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.shabinder.spotiflyer.worker.ForegroundService import com.shabinder.spotiflyer.worker.ForegroundService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -50,23 +46,34 @@ import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
fun loadAllImages(context: Context?, images:ArrayList<String>? = null ) { fun loadAllImages(context: Context?, images:List<String>? = null,source:Source) {
val serviceIntent = Intent(context, ForegroundService::class.java) val serviceIntent = Intent(context, ForegroundService::class.java)
images?.let { serviceIntent.putStringArrayListExtra("imagesList",it) } images?.let { serviceIntent.putStringArrayListExtra("imagesList",(it + source.name) as ArrayList<String>) }
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun startService(context:Context?,objects:ArrayList<DownloadObject>? = null ) { fun downloadTracks(
trackList: ArrayList<TrackDetails>,
context: Context? = mainActivity
) {
if(!trackList.isNullOrEmpty()){
val serviceIntent = Intent(context, ForegroundService::class.java) val serviceIntent = Intent(context, ForegroundService::class.java)
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) } serviceIntent.putParcelableArrayListExtra("object",trackList)
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
}
fun queryActiveTracks(context:Context? = mainActivity) {
val serviceIntent = Intent(context, ForegroundService::class.java).apply {
action = "query"
}
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{ fun finalOutputDir(itemName:String ,type:String, subFolder:String,extension:String = ".mp3"): String{
return Environment.getExternalStorageDirectory().toString() + File.separator + return defaultDir + removeIllegalChars(type) + File.separator +
defaultDir + removeIllegalChars(type) + File.separator + if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + File.separator} +
(if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} removeIllegalChars(itemName) + extension
+ itemName?.let { removeIllegalChars(it) + extension})
} }
/** /**
@ -115,19 +122,6 @@ fun showMessage(message: String, long: Boolean = false,isSuccess:Boolean = false
} }
} }
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(){ fun showNoConnectionAlert(){
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
mainActivity.apply { mainActivity.apply {
@ -172,26 +166,22 @@ fun bindImage(imgView: ImageView, imgUrl: String?,source: Source?) {
val file = when(source){ val file = when(source){
Source.Spotify->{ Source.Spotify->{
File( File(
Environment.getExternalStorageDirectory(), imageDir + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
) )
} }
Source.YouTube->{ Source.YouTube->{
//Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg" //Url Format: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
// We Are Naming using "$searchId" // We Are Naming using "$searchId"
File( File(
Environment.getExternalStorageDirectory(), imageDir + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
defaultDir+".Images/" + imgUrl.substringBeforeLast('/',imgUrl).substringAfterLast('/',imgUrl) + ".jpeg"
) )
} }
Source.Gaana -> { Source.Gaana -> {
File( File(
Environment.getExternalStorageDirectory(), imageDir + (imgUrl.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
Provider.defaultDir +".Images/" + (imgUrl.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
} }
else -> File( else -> File(
Environment.getExternalStorageDirectory(), imageDir + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
defaultDir+".Images/" + imgUrl.substringAfterLast('/',imgUrl) + ".jpeg"
) )
} }
// the File to save , append increasing numeric counter to prevent files from getting overwritten. // the File to save , append increasing numeric counter to prevent files from getting overwritten.
@ -221,23 +211,22 @@ fun File.copyTo(file: File) {
} }
} }
fun createDirectory(dir:String){ fun createDirectory(dir:String){
val yourAppDir = File(Environment.getExternalStorageDirectory(), val yourAppDir = File(dir)
dir)
if(!yourAppDir.exists() && !yourAppDir.isDirectory) if(!yourAppDir.exists() && !yourAppDir.isDirectory)
{ // create empty directory { // create empty directory
if (yourAppDir.mkdirs()) if (yourAppDir.mkdirs())
{Log.i("CreateDir","App dir created")} {Log.i("CreateDir","$dir created")}
else else
{Log.w("CreateDir","Unable to create app dir!")} {Log.w("CreateDir","Unable to create Dir: $dir!")}
} }
else else
{Log.i("CreateDir","App dir already exists")} {Log.i("CreateDir","$dir already exists")}
} }
/** /**
* Removing Illegal Chars from File Name * Removing Illegal Chars from File Name
* **/ * **/
fun removeIllegalChars(fileName: String): String? { fun removeIllegalChars(fileName: String): String {
val illegalCharArray = charArrayOf( val illegalCharArray = charArrayOf(
'/', '/',
'\n', '\n',
@ -277,28 +266,9 @@ fun removeIllegalChars(fileName: String): String? {
fun createDirectories() { fun createDirectories() {
createDirectory(defaultDir) createDirectory(defaultDir)
createDirectory(defaultDir + ".Images/") createDirectory(imageDir)
createDirectory(defaultDir + "Tracks/") createDirectory(defaultDir + "Tracks/")
createDirectory(defaultDir + "Albums/") createDirectory(defaultDir + "Albums/")
createDirectory(defaultDir + "Playlists/") createDirectory(defaultDir + "Playlists/")
createDirectory(defaultDir + "YT_Downloads/") createDirectory(defaultDir + "YT_Downloads/")
} }
fun getEmojiByUnicode(unicode: Int): String? {
return String(Character.toChars(unicode))
}
/*
internal val nullOnEmptyConverterFactory = object : Converter.Factory() {
fun converterFactory() = this
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
) = object : Converter<ResponseBody, Any?> {
val nextResponseBodyConverter =
retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
override fun convert(value: ResponseBody) =
if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
}
}*/

View File

@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.util.Log import android.util.Log
@ -45,21 +46,30 @@ import com.github.kiulian.downloader.model.quality.AudioQuality
import com.mpatric.mp3agic.ID3v1Tag import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.MainActivity
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.downloadHelper.getYTTracks
import com.shabinder.spotiflyer.downloadHelper.sortByBestMatch
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.copyTo import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.tonyodev.fetch2.* import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.* import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import javax.inject.Inject
@Suppress("DEPRECATION") @AndroidEntryPoint
@RequiresApi(Build.VERSION_CODES.O)
class ForegroundService : Service(){ class ForegroundService : Service(){
private val tag = "Foreground Service" private val tag = "Foreground Service"
private val channelId = "ForegroundDownloaderService" private val channelId = "ForegroundDownloaderService"
@ -67,61 +77,59 @@ class ForegroundService : Service(){
private var total = 0 //Total Downloads Requested private var total = 0 //Total Downloads Requested
private var converted = 0//Total Files Converted private var converted = 0//Total Files Converted
private var downloaded = 0//Total Files downloaded private var downloaded = 0//Total Files downloaded
private lateinit var fetch:Fetch
private lateinit var ytDownloader: YoutubeDownloader
private lateinit var downloadManager : DownloadManager
private var serviceJob = Job() private var serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val requestMap = mutableMapOf<Request, TrackDetails>() private val requestMap = mutableMapOf<Request, TrackDetails>()
private var speed :Long = 0 private val allTracksDetails = mutableListOf<TrackDetails>()
private var defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator private var defaultDir = Provider.defaultDir
private val parentDirectory = File(Environment.getExternalStorageDirectory(),
defaultDir +File.separator
)
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false private var isServiceStarted = false
var notificationLine = 0 private var notificationLine = 0
val messageList = mutableListOf("","","","") private var messageList = mutableListOf("", "", "", "")
private var pendingIntent:PendingIntent? = null private lateinit var cancelIntent:PendingIntent
private lateinit var fetch:Fetch
private lateinit var ytDownloader: YoutubeDownloader
private lateinit var downloadManager : DownloadManager
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
override fun onBind(intent: Intent): IBinder? = null
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val notificationIntent = Intent(this, MainActivity::class.java) createNotificationChannel(channelId,"Downloader Service")
pendingIntent = PendingIntent.getActivity( val intent = Intent(
this, this,
0, notificationIntent, 0 ForegroundService::class.java
) ).apply{action = "kill"}
cancelIntent = PendingIntent.getService (this, 0 , intent , PendingIntent.FLAG_CANCEL_CURRENT )
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
ytDownloader = YoutubeDownloader() ytDownloader = YoutubeDownloader()
val fetchConfiguration = initialiseFetch()
FetchConfiguration.Builder(this)
.setDownloadConcurrentLimit(4)
.build()
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
fetch = Fetch.getDefaultInstance()
fetch.addListener(fetchListener)
//clearing all not completed Downloads
//Starting fresh
fetch.removeAll()
startForeground()
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started // Send a notification that service is started
Log.i(tag,"Service Started.") Log.i(tag, "Service Started.")
startForeground() startForeground(notificationId, getNotification())
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList("object"))
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList("imagesList")) when (intent.action) {
"kill" -> killService()
"query" -> {
val response = Intent().apply {
action = "query_result"
putParcelableArrayListExtra("tracks", allTracksDetails as ArrayList<TrackDetails> )
}
sendBroadcast(response)
}
}
val downloadObjects: ArrayList<TrackDetails>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList(
"object"
))
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList(
"imagesList"
))
imagesList?.let{ imagesList?.let{
serviceScope.launch { serviceScope.launch {
@ -132,7 +140,10 @@ class ForegroundService : Service(){
downloadObjects?.let { downloadObjects?.let {
total += downloadObjects.size total += downloadObjects.size
updateNotification() updateNotification()
downloadAllTracks(downloadObjects) it.forEach { it1 ->
allTracksDetails.add(it1.apply { downloaded = DownloadStatus.Queued })
}
downloadAllTracks(it)
} }
//Wake locks and misc tasks from here : //Wake locks and misc tasks from here :
@ -140,7 +151,7 @@ class ForegroundService : Service(){
//Service Already Started //Service Already Started
START_STICKY START_STICKY
} else{ } else{
Log.i(tag,"Starting the foreground service task") Log.i(tag, "Starting the foreground service task")
isServiceStarted = true isServiceStarted = true
wakeLock = wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run { (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
@ -152,11 +163,59 @@ class ForegroundService : Service(){
} }
} }
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){ /**
* Function To Download All Tracks Available in a List
**/
private fun downloadAllTracks(trackList: List<TrackDetails>) {
serviceScope.launch(Dispatchers.Default){
trackList.forEach {
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
}else {
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
downloadTrack(it.videoID!!, it)
} else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
object : Callback<String> {
override fun onResponse(
call: Call<String>,
response: Response<String>
) {
val videoId = sortByBestMatch(
getYTTracks(response.body().toString()),
trackName = it.title,
trackArtists = it.artists,
trackDurationSec = it.durationSec
).keys.firstOrNull()
Log.i("Service VideoID", videoId ?: "Not Found")
if (videoId.isNullOrBlank()) sendTrackBroadcast(
Status.FAILED.name,
it
)
else {//Found Youtube Video ID
downloadTrack(videoId, it)
}
}
override fun onFailure(call: Call<String>, 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())
}
}
)
}
}
}
}
}
fun downloadTrack(videoID:String,track: TrackDetails){
serviceScope.launch(Dispatchers.IO) { serviceScope.launch(Dispatchers.IO) {
for(downloadObj in downloadObjects){
try { try {
val video = ytDownloader.getVideo(downloadObj.ytVideoId) val video = ytDownloader.getVideo(videoID)
val format: Format? = try { val format: Format? = try {
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) { } catch (e: java.lang.IndexOutOfBoundsException) {
@ -174,53 +233,25 @@ class ForegroundService : Service(){
format?.let { format?.let {
val url: String = format.url() val url: String = format.url()
Log.i("DHelper Link Found", url) Log.i("DHelper Link Found", url)
serviceScope.launch { val request= Request(url, track.outputFile).apply{
val request= Request(url, downloadObj.outputFile) priority = Priority.NORMAL
request.priority = Priority.NORMAL networkType = NetworkType.ALL
request.networkType = NetworkType.ALL }
fetch.enqueue(request, fetch.enqueue(request,
{ {
requestMap[it] = downloadObj.trackDetails requestMap[it] = track
Log.i(tag, "Enqueuing Download") Log.i(tag, "Enqueuing Download")
}, },
{ {
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")} Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
)
} }
)
} }
}catch (e: com.github.kiulian.downloader.YoutubeException){ }catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString()) Log.i("Service YT Error", e.message.toString())
} }
} }
} }
}
override fun onDestroy() {
super.onDestroy()
if(converted == total){
Handler().postDelayed({
Log.i(tag,"Service destroyed.")
deleteFile(parentDirectory)
releaseWakeLock()
stopForeground(true)
},2000)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(converted == total ){
Log.i(tag,"Service Removed.")
deleteFile(parentDirectory)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
stopSelf()//System will automatically close it
}
}
}
/** /**
* Fetch Listener/ Responsible for Fetch Behaviour * Fetch Listener/ Responsible for Fetch Behaviour
@ -230,7 +261,7 @@ class ForegroundService : Service(){
download: Download, download: Download,
waitingOnNetwork: Boolean waitingOnNetwork: Boolean
) { ) {
// TODO("Not yet implemented") requestMap[download.request]?.let { sendTrackBroadcast(Status.QUEUED.name, it) }
} }
override fun onRemoved(download: Download) { override fun onRemoved(download: Download) {
@ -256,7 +287,7 @@ class ForegroundService : Service(){
messageList[1] = "Downloading ${track?.title}" messageList[1] = "Downloading ${track?.title}"
notificationLine = 2 notificationLine = 2
} }
2-> { 2 -> {
messageList[2] = "Downloading ${track?.title}" messageList[2] = "Downloading ${track?.title}"
notificationLine = 3 notificationLine = 3
} }
@ -265,8 +296,13 @@ class ForegroundService : Service(){
notificationLine = 0 notificationLine = 0
} }
} }
Log.i(tag,"${track?.title} Download Started") Log.i(tag, "${track?.title} Download Started")
track?.let{
allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(it.title)] =
it.apply { downloaded = DownloadStatus.Downloading }
updateNotification() updateNotification()
sendTrackBroadcast(Status.DOWNLOADING.name,track)
}
} }
override fun onWaitingNetwork(download: Download) { override fun onWaitingNetwork(download: Download) {
@ -292,19 +328,22 @@ class ForegroundService : Service(){
serviceScope.launch { serviceScope.launch {
try{ try{
track?.let { convertToMp3(download.file, it) } track?.let {
Log.i(tag,"${track?.title} Download Completed") convertToMp3(download.file, it)
}catch (e:KotlinNullPointerException allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(it.title)] =
it.apply { downloaded = DownloadStatus.Converting }
}
Log.i(tag, "${track?.title} Download Completed")
}catch (
e: KotlinNullPointerException
){ ){
Log.i(tag,"${track?.title} Download Failed! Error:Fetch!!!!") Log.i(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
Log.i(tag,"${track?.title} Requesting Download thru Android DM") Log.i(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++ downloaded++
requestMap.remove(download.request) requestMap.remove(download.request)
} }
} }
speed = 0
// updateNotification()
} }
override fun onDeleted(download: Download) { override fun onDeleted(download: Download) {
@ -323,9 +362,9 @@ class ForegroundService : Service(){
serviceScope.launch { serviceScope.launch {
val track = requestMap[download.request] val track = requestMap[download.request]
downloaded++ downloaded++
Log.i(tag,download.error.throwable.toString()) Log.i(tag, download.error.throwable.toString())
Log.i(tag,"${track?.title} Requesting Download thru Android DM") Log.i(tag, "${track?.title} Requesting Download thru Android DM")
downloadUsingDM(download.request.url,download.request.file, track!!) downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request) requestMap.remove(download.request)
} }
updateNotification() updateNotification()
@ -341,30 +380,33 @@ class ForegroundService : Service(){
downloadedBytesPerSecond: Long downloadedBytesPerSecond: Long
) { ) {
val track = requestMap[download.request] val track = requestMap[download.request]
Log.i(tag,"${track?.title} ETA: ${etaInMilliSeconds/1000} sec") Log.i(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
speed = (downloadedBytesPerSecond/1000) val intent = Intent().apply {
// updateNotification() action = "Progress"
putExtra("progress", download.progress)
putExtra("track", requestMap[download.request])
}
sendBroadcast(intent)
} }
} }
/** /**
* If fetch Fails , Android Download Manager To RESCUE!! * If fetch Fails , Android Download Manager To RESCUE!!
**/ **/
fun downloadUsingDM(url:String, outputDir:String, track: TrackDetails){ fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
val uri = Uri.parse(url) val uri = Uri.parse(url)
val request = DownloadManager.Request(uri) val request = DownloadManager.Request(uri).apply {
.setAllowedNetworkTypes( setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE DownloadManager.Request.NETWORK_MOBILE
) )
.setAllowedOverRoaming(false) setAllowedOverRoaming(false)
.setTitle(track.title) setTitle(track.title)
.setDescription("Spotify Downloader Working Up here...") setDescription("Spotify Downloader Working Up here...")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir.removePrefix( setDestinationUri(File(outputDir).toUri())
Environment.getExternalStorageDirectory().toString() + Environment.DIRECTORY_MUSIC + File.separator setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
)) }
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
//Start Download //Start Download
val downloadID = downloadManager.enqueue(request) val downloadID = downloadManager.enqueue(request)
Log.i("DownloadManager", "Download Request Sent") Log.i("DownloadManager", "Download Request Sent")
@ -375,20 +417,23 @@ class ForegroundService : Service(){
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
//Checking if the received broadcast is for our enqueued download by matching download id //Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) { if (downloadID == id) {
convertToMp3(outputDir,track) allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(track.title)] =
track.apply { downloaded = DownloadStatus.Converting }
convertToMp3(outputDir, track)
converted++ converted++
//Unregister this broadcast Receiver //Unregister this broadcast Receiver
this@ForegroundService.unregisterReceiver(this) this@ForegroundService.unregisterReceiver(this)
} }
} }
} }
registerReceiver(onDownloadComplete,IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
} }
/** /**
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata) *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/ **/
fun convertToMp3(filePath: String, track: TrackDetails){ fun convertToMp3(filePath: String, track: TrackDetails){
sendTrackBroadcast("Converting",track)
val m4aFile = File(filePath) val m4aFile = File(filePath)
FFmpeg.executeAsync( FFmpeg.executeAsync(
@ -398,39 +443,41 @@ class ForegroundService : Service(){
RETURN_CODE_SUCCESS -> { RETURN_CODE_SUCCESS -> {
Log.i(Config.TAG, "Async command execution completed successfully.") Log.i(Config.TAG, "Async command execution completed successfully.")
m4aFile.delete() m4aFile.delete()
writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track) writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
//FFMPEG task Completed //FFMPEG task Completed
} }
RETURN_CODE_CANCEL -> { RETURN_CODE_CANCEL -> {
Log.i(Config.TAG, "Async command execution cancelled by user.") Log.i(Config.TAG, "Async command execution cancelled by user.")
} }
else -> { else -> {
Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode)) Log.i(
Config.TAG, String.format(
"Async command execution failed with rc=%d.",
returnCode
)
)
} }
} }
} }
} }
private fun writeMp3Tags(filePath:String, track: TrackDetails){ private fun writeMp3Tags(filePath: String, track: TrackDetails){
var mp3File = Mp3File(filePath) var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File) mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File,track) mp3File = setId3v1Tags(mp3File, track)
mp3File = setId3v2Tags(mp3File,track) mp3File = setId3v2Tags(mp3File, track)
Log.i("Mp3Tags","saving file") Log.i("Mp3Tags", "saving file")
mp3File.save(filePath.substringBeforeLast('.')+".new.mp3") mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
val file = File(filePath) val file = File(filePath)
file.delete() file.delete()
val newFile = File((filePath.substringBeforeLast('.')+".new.mp3")) val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
newFile.renameTo(file) newFile.renameTo(file)
converted++ converted++
updateNotification() updateNotification()
addToLibrary(file.absolutePath)
allTracksDetails.removeAt(allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(track.title))
//Notify Download Completed //Notify Download Completed
val intent = Intent() sendTrackBroadcast("track_download_completed",track)
.setAction("track_download_completed")
.putExtra("track",track)
this@ForegroundService.sendBroadcast(intent)
//All tasks completed (REST IN PEACE) //All tasks completed (REST IN PEACE)
if(converted == total){ if(converted == total){
onDestroy() onDestroy()
@ -443,19 +490,7 @@ class ForegroundService : Service(){
private fun updateNotification() { private fun updateNotification() {
val mNotificationManager: NotificationManager = val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, channelId) mNotificationManager.notify(notificationId, getNotification())
.setSmallIcon(R.drawable.down_arrowbw)
.setSubText("Total: $total Completed:$converted")
.setNotificationSilent()
.setStyle(NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0])
.addLine(messageList[1])
.addLine(messageList[2])
.addLine(messageList[3]))
.setContentIntent(pendingIntent)
.build()
mNotificationManager.notify(notificationId, notification)
} }
/** /**
@ -487,9 +522,9 @@ class ForegroundService : Service(){
val fis = FileInputStream(track.albumArt) val fis = FileInputStream(track.albumArt)
fis.read(bytesArray) //read file into bytes[] fis.read(bytesArray) //read file into bytes[]
fis.close() fis.close()
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg") id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
}catch (e:java.io.FileNotFoundException){ }catch (e: java.io.FileNotFoundException){
Log.i("Error","Couldn't Write Mp3 Album Art") Log.i("Error", "Couldn't Write Mp3 Album Art")
} }
mp3file.id3v2Tag = id3v2Tag mp3file.id3v2Tag = id3v2Tag
return mp3file return mp3file
@ -509,7 +544,7 @@ class ForegroundService : Service(){
} }
private fun releaseWakeLock() { private fun releaseWakeLock() {
Log.i(tag,"Releasing Wake Lock") Log.i(tag, "Releasing Wake Lock")
try { try {
wakeLock?.let { wakeLock?.let {
if (it.isHeld) { if (it.isHeld) {
@ -517,63 +552,36 @@ class ForegroundService : Service(){
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.i(tag,"Service stopped without being started: ${e.message}") Log.i(tag, "Service stopped without being started: ${e.message}")
} }
isServiceStarted = false 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") @Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{ private fun createNotificationChannel(channelId: String, channelName: String){
val chan = NotificationChannel(channelId, val channel = NotificationChannel(
channelName, NotificationManager.IMPORTANCE_DEFAULT) channelId,
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC channelName, NotificationManager.IMPORTANCE_DEFAULT
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan) service.createNotificationChannel(channel)
return channelId
} }
/** /**
* Deleting All Residual Files except Mp3 Files * Cleaning All Residual Files except Mp3 Files
**/ **/
private fun deleteFile(dir:File) { private fun cleanFiles(dir: File) {
Log.i(tag,"Starting Deletions in ${dir.path} ") Log.i(tag, "Starting Cleaning in ${dir.path} ")
val fList = dir.listFiles() val fList = dir.listFiles()
fList?.let { fList?.let {
for (file in fList) { for (file in fList) {
if (file.isDirectory) { if (file.isDirectory) {
deleteFile(file) cleanFiles(file)
} else if(file.isFile) { } else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){ if(file.path.toString().substringAfterLast(".") != "mp3"){
Log.i(tag,"deleting ${file.path}") Log.i(tag, "Cleaning ${file.path}")
file.delete() file.delete()
} }
} }
@ -581,6 +589,15 @@ class ForegroundService : Service(){
} }
} }
/*
* Add File to Android's Media Library.
* */
private fun addToLibrary(path:String) {
Log.i(tag,"Scanning File")
MediaScannerConnection.scanFile(this,
listOf(path).toTypedArray(), null,null)
}
/** /**
* Function to fetch all Images for use in mp3 tags. * Function to fetch all Images for use in mp3 tags.
**/ **/
@ -589,20 +606,20 @@ class ForegroundService : Service(){
* Last Element of this List defines Its Source * Last Element of this List defines Its Source
* */ * */
val source = urlList.last() val source = urlList.last()
for (url in urlList.subList(0,urlList.size-2)) { for (url in urlList.subList(0, urlList.size - 2)) {
val imgUri = url.toUri().buildUpon().scheme("https").build() val imgUri = url.toUri().buildUpon().scheme("https").build()
Glide Glide
.with(this) .with(this)
.asFile() .asFile()
.load(imgUri) .load(imgUri)
.listener(object: RequestListener<File> { .listener(object : RequestListener<File> {
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
target: Target<File>?, target: Target<File>?,
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
Log.i("Glide","LoadFailed") Log.i("Glide", "LoadFailed")
return false return false
} }
@ -614,29 +631,35 @@ class ForegroundService : Service(){
isFirstResource: Boolean isFirstResource: Boolean
): Boolean { ): Boolean {
serviceScope.launch { serviceScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO) {
try { try {
val file = when(source){ val file = when (source) {
"spotify" ->{ Source.Spotify.name -> {
File(imageDir, url.substringAfterLast('/') + ".jpeg")
}
Source.YouTube.name -> {
File( File(
Environment.getExternalStorageDirectory(), imageDir,
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg" url.substringBeforeLast('/', url)
.substringAfterLast(
'/',
url
) + ".jpeg"
) )
} }
"youtube" ->{ Source.Gaana.name -> {
File( File(
Environment.getExternalStorageDirectory(), imageDir,
defaultDir +".Images/" + url.substringBeforeLast('/',url).substringAfterLast('/',url) + ".jpeg" (url.substringBeforeLast('/').substringAfterLast(
'/'
)) + ".jpeg"
) )
} }
"gaana" -> {
File(
Environment.getExternalStorageDirectory(),
Provider.defaultDir +".Images/" + (url.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg")
}
else -> File( else -> File(
Environment.getExternalStorageDirectory(), imageDir,
defaultDir +".Images/" + url.substringAfterLast('/') + ".jpeg") url.substringAfterLast('/') + ".jpeg"
)
} }
resource?.copyTo(file) resource?.copyTo(file)
} catch (e: IOException) { } catch (e: IOException) {
@ -650,4 +673,77 @@ class ForegroundService : Service(){
} }
} }
private fun killService() {
serviceScope.launch{
Log.i(tag,"Killing Self")
messageList = mutableListOf("Cleaning And Exiting","","","")
fetch.cancelAll()
fetch.removeAll()
updateNotification()
cleanFiles(File(defaultDir))
messageList = mutableListOf("","","","")
releaseWakeLock()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
stopSelf()//System will automatically close it
}
}
}
override fun onDestroy() {
super.onDestroy()
if(converted == total){
Handler(Looper.myLooper()!!).postDelayed({
killService()
}, 5000)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(converted == total ){
killService()
}
}
private fun initialiseFetch() {
val fetchConfiguration =
FetchConfiguration.Builder(this)
.setNamespace(channelId)
.setDownloadConcurrentLimit(4)
.build()
Fetch.setDefaultInstanceConfiguration(fetchConfiguration)
fetch = Fetch.getDefaultInstance()
fetch.addListener(fetchListener)
//clearing all not completed Downloads
//Starting fresh
fetch.removeAll()
}
private fun getNotification():Notification = NotificationCompat.Builder(this, channelId).run {
setSmallIcon(R.drawable.down_arrowbw)
setSubText("Total: $total Completed:$converted")
setNotificationSilent()
setStyle(
NotificationCompat.InboxStyle().run {
addLine(messageList[0])
addLine(messageList[1])
addLine(messageList[2])
addLine(messageList[3])
}
)
addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
build()
}
fun sendTrackBroadcast(action:String,track:TrackDetails){
val intent = Intent().apply{
setAction(action)
putExtra("track", track)
}
this@ForegroundService.sendBroadcast(intent)
}
} }

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2020 Shabinder Singh
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:innerRadiusRatio="2.5"
android:shape="ring"
android:thickness="4dp"
android:useLevel="false">
<solid android:color="@color/colorPrimary"/>
</shape>

View File

@ -15,8 +15,8 @@
~ along with this program. If not, see <https://www.gnu.org/licenses/>. ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="26dp" <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="36dp"
android:height="22dp" android:viewportWidth="512" android:viewportHeight="512"> android:height="32dp" android:viewportWidth="512" android:viewportHeight="512">
<path android:fillColor="#516AEC" android:pathData="m296,288 l60,-60c7.73,-7.73 17.86,-11.6 28,-11.6 10.055,0 20.101,3.806 27.806,11.407 15.612,15.402 15.207,41.18 -0.3,56.687l-134.293,134.293c-11.716,11.716 -30.711,11.716 -42.426,0l-134.787,-134.787c-7.73,-7.73 -11.6,-17.86 -11.6,-28 0,-10.055 3.806,-20.101 11.407,-27.806 15.402,-15.612 41.18,-15.207 56.687,0.3l59.506,59.506v-232c0,-22.091 17.909,-40 40,-40 22.091,0 40,17.909 40,40z"/> <path android:fillColor="#516AEC" android:pathData="m296,288 l60,-60c7.73,-7.73 17.86,-11.6 28,-11.6 10.055,0 20.101,3.806 27.806,11.407 15.612,15.402 15.207,41.18 -0.3,56.687l-134.293,134.293c-11.716,11.716 -30.711,11.716 -42.426,0l-134.787,-134.787c-7.73,-7.73 -11.6,-17.86 -11.6,-28 0,-10.055 3.806,-20.101 11.407,-27.806 15.402,-15.612 41.18,-15.207 56.687,0.3l59.506,59.506v-232c0,-22.091 17.909,-40 40,-40 22.091,0 40,17.909 40,40z"/>
<path android:fillColor="#EC7EBA" android:pathData="m411.51,284.49 l-134.3,134.3c-11.71,11.71 -30.71,11.71 -42.42,0l-12.74,-12.74c10.69,4.06 23.23,1.77 31.84,-6.84l134.29,-134.29c12.51,-12.51 15.19,-31.7 7.57,-46.74 5.86,1.81 11.39,5.03 16.06,9.63 15.61,15.4 15.2,41.18 -0.3,56.68z"/> <path android:fillColor="#EC7EBA" android:pathData="m411.51,284.49 l-134.3,134.3c-11.71,11.71 -30.71,11.71 -42.42,0l-12.74,-12.74c10.69,4.06 23.23,1.77 31.84,-6.84l134.29,-134.29c12.51,-12.51 15.19,-31.7 7.57,-46.74 5.86,1.81 11.39,5.03 16.06,9.63 15.61,15.4 15.2,41.18 -0.3,56.68z"/>
<path android:fillColor="#EC7EBA" android:pathData="m251.88,27.72c-3.46,-3.46 -7.55,-6.29 -12.08,-8.3 4.95,-2.2 10.43,-3.42 16.2,-3.42 11.04,0 21.04,4.48 28.28,11.72s11.72,17.24 11.72,28.28v232l-15.329,15.329c-6.3,6.3 -17.071,1.838 -17.071,-7.071v-240.258c0,-11.04 -4.48,-21.04 -11.72,-28.28z"/> <path android:fillColor="#EC7EBA" android:pathData="m251.88,27.72c-3.46,-3.46 -7.55,-6.29 -12.08,-8.3 4.95,-2.2 10.43,-3.42 16.2,-3.42 11.04,0 21.04,4.48 28.28,11.72s11.72,17.24 11.72,28.28v232l-15.329,15.329c-6.3,6.3 -17.071,1.838 -17.071,-7.071v-240.258c0,-11.04 -4.48,-21.04 -11.72,-28.28z"/>

View File

@ -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 <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
</vector>

View File

@ -0,0 +1,32 @@
<!--
~ Copyright (C) 2020 Shabinder Singh
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:width="38dp" android:height="38dp"
android:viewportWidth="512" android:viewportHeight="512">
<path android:pathData="m512,256c0,141.387 -114.613,256 -256,256s-256,-114.613 -256,-256 114.613,-256 256,-256 256,114.613 256,256zM512,256">
<aapt:attr name="android:fillColor">
<gradient android:endX="512" android:endY="256"
android:startX="0" android:startY="256" android:type="linear">
<item android:color="#748AFF" android:offset="0"/>
<item android:color="#FF3C64" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:fillColor="#000" android:pathData="m256,56c-110.281,0 -200,89.719 -200,200s89.719,200 200,200 200,-89.719 200,-200 -89.719,-200 -200,-200zM256,426c-93.738,0 -170,-76.262 -170,-170s76.262,-170 170,-170 170,76.262 170,170 -76.262,170 -170,170zM256,426"/>
<path android:fillColor="#000" android:pathData="m324.18,187.82c-5.859,-5.855 -15.355,-5.855 -21.215,0l-46.965,46.965 -46.965,-46.965c-5.859,-5.855 -15.355,-5.855 -21.215,0 -5.855,5.859 -5.855,15.355 0,21.215l46.965,46.965 -46.965,46.965c-5.855,5.859 -5.855,15.355 0,21.215 2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.605,-4.395l46.969,-46.965 46.965,46.965c2.93,2.93 6.77,4.395 10.605,4.395 3.84,0 7.68,-1.465 10.609,-4.395 5.855,-5.859 5.855,-15.355 0,-21.215l-46.965,-46.965 46.965,-46.965c5.855,-5.859 5.855,-15.355 0,-21.215zM324.18,187.82"/>
</vector>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2020 Shabinder Singh
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#ff9d9e9d"
android:centerY="0.75"
android:endColor="#ff9d9e9d"
android:startColor="#ff9d9e9d"
/>
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="5dip" />
<gradient
android:angle="270"
android:centerColor="#80ffb600"
android:centerY="0.75"
android:endColor="#80ffb600"
android:startColor="#80ffb600"
/>
</shape>
</clip>
</item>
<item
android:id="@android:id/progress"
>
<clip>
<shape>
<corners
android:radius="5dip" />
<gradient
android:angle="270"
android:endColor="#2196f3"
android:startColor="#2196f3" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (C) 2020 Shabinder Singh
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/titleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="0dp"
android:layout_height="5dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/progress_bar"
app:layout_constraintBottom_toBottomOf="@+id/actionButton"
app:layout_constraintEnd_toStartOf="@+id/actionButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/actionButton" />
<Button
android:id="@+id/actionButton"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="Retry"
android:textColor="@color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/titleTextView"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/progress_TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
app:layout_constraintEnd_toStartOf="@+id/actionButton"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/progressBar"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
tools:text="10%" />
<TextView
android:id="@+id/downloadSpeedTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/status_TextView"
app:layout_constraintEnd_toEndOf="@+id/progressBar"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/progress_TextView"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
tools:text="204 MB/s" />
<TextView
android:id="@+id/remaining_TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/status_TextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progress_TextView"
tools:text="10s" />
<TextView
android:id="@+id/status_TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textStyle="italic|bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/remaining_TextView"
app:layout_constraintTop_toBottomOf="@+id/actionButton"
tools:text="Status" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@android:color/darker_gray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -41,10 +41,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="48dp" android:layout_marginBottom="48dp"
android:background="@drawable/text_background_accented" android:background="@drawable/text_background_accented"
android:fontFamily="@font/raleway_semibold"
android:foreground="@drawable/rounded_gradient" android:foreground="@drawable/rounded_gradient"
android:padding="7dp" android:padding="7dp"
android:text="Developer: Shabinder Singh" android:text=" Developer: Shabinder Singh "
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="16sp" android:textSize="16sp"
android:visibility="visible" android:visibility="visible"

View File

@ -54,7 +54,7 @@
app:layout_anchor="@+id/appbar" app:layout_anchor="@+id/appbar"
app:layout_anchorGravity="bottom|center" app:layout_anchorGravity="bottom|center"
app:maxImageSize="38dp" app:maxImageSize="38dp"
app:rippleColor="@color/colorPrimaryDark" android:clickable="false"
app:srcCompat="@drawable/ic_refresh" app:srcCompat="@drawable/ic_refresh"
app:tint="@null" /> app:tint="@null" />
@ -116,19 +116,19 @@
app:layout_constraintBottom_toTopOf="@+id/cover_image" app:layout_constraintBottom_toTopOf="@+id/cover_image"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
android:id="@+id/title_view" android:id="@+id/title_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2dp" android:layout_marginBottom="24dp"
android:layout_marginBottom="22dp"
android:background="#00000000" android:background="#00000000"
android:fontFamily="@font/raleway_semibold" android:fontFamily="@font/raleway_semibold"
android:gravity="end" android:gravity="end"
android:text='"Loading..."' android:text='"Loading..."'
android:textAlignment="viewEnd" android:textAlignment="viewEnd"
android:textColor="#9AB3FF" android:textColor="#9AB3FF"
android:textSize="28sp" android:textSize="26sp"
android:textStyle="bold" android:textStyle="bold"
android:visibility="visible" android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -19,14 +19,14 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="80dp" android:layout_height="wrap_content"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:background="#000000"> android:background="#000000">
<ImageView <ImageView
android:id="@+id/imageUrl" android:id="@+id/imageUrl"
android:layout_width="100dp" android:layout_width="90dp"
android:layout_height="80dp" android:layout_height="70dp"
android:contentDescription="Track Image" android:contentDescription="Track Image"
android:scaleType="centerInside" android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -41,6 +41,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="14dp" android:layout_marginTop="14dp"
android:layout_marginEnd="8dp"
android:fontFamily="@font/raleway_semibold" android:fontFamily="@font/raleway_semibold"
android:letterSpacing="0.04" android:letterSpacing="0.04"
android:lines="1" android:lines="1"
@ -48,9 +49,9 @@
android:textAllCaps="false" android:textAllCaps="false"
android:textAppearance="@style/TextAppearance.AppTheme.Headline4" android:textAppearance="@style/TextAppearance.AppTheme.Headline4"
android:textColor="#9AB3FF" android:textColor="#9AB3FF"
android:textSize="20sp" android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/btn_download" app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
app:layout_constraintStart_toStartOf="@+id/artist" app:layout_constraintStart_toEndOf="@+id/imageUrl"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
@ -61,7 +62,6 @@
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:paddingLeft="9dp"
android:text="Alan Walker" android:text="Alan Walker"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -75,12 +75,11 @@
style="@style/TextAppearance.AppCompat.Body2" style="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="3dp" android:layout_marginEnd="16dp"
android:paddingLeft="9dp"
android:text="4 minutes, 20 sec" android:text="4 minutes, 20 sec"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/artist" app:layout_constraintBottom_toBottomOf="@+id/artist"
app:layout_constraintEnd_toStartOf="@+id/btn_download" app:layout_constraintEnd_toStartOf="@+id/btn_download_progress"
app:layout_constraintStart_toEndOf="@+id/artist" app:layout_constraintStart_toEndOf="@+id/artist"
app:layout_constraintTop_toTopOf="@+id/artist" /> app:layout_constraintTop_toTopOf="@+id/artist" />
@ -88,12 +87,36 @@
android:id="@+id/btn_download" android:id="@+id/btn_download"
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circular_background"
android:backgroundTint="@color/black" android:backgroundTint="@color/black"
android:scaleType="fitCenter" android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow" /> app:srcCompat="@drawable/ic_arrow" />
<com.github.lzyzsd.circleprogress.ArcProgress
android:id="@+id/btn_download_progress"
android:layout_width="55dp"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="1dp"
android:visibility="invisible"
app:arc_angle="260"
app:arc_bottom_text="Waiting"
app:arc_bottom_text_size="9sp"
app:arc_finished_color="@color/colorPrimary"
app:arc_progress="0"
app:arc_stroke_width="2dp"
app:arc_suffix_text_padding="0dp"
app:arc_suffix_text_size="11sp"
app:arc_text_color="@color/colorPrimary"
app:arc_text_size="20sp"
app:arc_unfinished_color="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,8 +18,8 @@
<AppUpdater> <AppUpdater>
<update> <update>
<latestVersion>1.6</latestVersion> <latestVersion>1.7</latestVersion>
<latestVersionCode>8</latestVersionCode> <latestVersionCode>9</latestVersionCode>
<url>https://github.com/Shabinder/SpotiFlyer/releases/</url> <url>https://github.com/Shabinder/SpotiFlyer/releases/</url>
</update> </update>
</AppUpdater> </AppUpdater>

View File

@ -18,7 +18,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext{ ext{
kotlin_version = "1.4.10" kotlin_version = "1.4.20"
navigationVersion = '2.3.0' navigationVersion = '2.3.0'
ext.hilt_version = '2.29.1-alpha' ext.hilt_version = '2.29.1-alpha'
} }
@ -28,12 +28,17 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.0' classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//safe-Args //Safe-Args
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
//Hilt
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
//Kotlinx-Serialization
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
//Crashlytics & Analytics
classpath 'com.google.gms:google-services:4.3.4'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }
@ -45,9 +50,6 @@ allprojects {
jcenter() jcenter()
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
flatDir {
dirs 'libs'
}
} }
} }

View File

@ -15,24 +15,20 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# #
# Project-wide Gradle settings. ## For more details on how to configure your build environment visit
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html # http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m # Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode. # When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the #Wed Dec 02 11:11:59 IST 2020
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
android.enableJetifier=true

View File

@ -1,19 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
configurations.maybeCreate("default")
artifacts.add("default", file('mobile-ffmpeg.aar'))

View File

@ -14,7 +14,5 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
include ':mobile-ffmpeg'
include ':app' include ':app'
rootProject.name = "spotiflyer" rootProject.name = "spotiflyer"