mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Merge pull request #20 from Shabinder/develop
Merge Changes From Develop to Origin v1.7
This commit is contained in:
commit
9d04e09d61
@ -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>
|
||||||
|
@ -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" />
|
||||||
|
103
app/build.gradle
103
app/build.gradle
@ -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'
|
||||||
|
@ -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">
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
}
|
}
|
@ -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,62 +47,89 @@ 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)
|
||||||
if(!isOnline()){
|
clearAnimation()
|
||||||
showNoConnectionAlert()
|
visible()
|
||||||
return@setOnClickListener
|
setOnClickListener{
|
||||||
}
|
if(!isOnline()){
|
||||||
showMessage("Processing!")
|
showNoConnectionAlert()
|
||||||
holder.binding.btnDownload.setImageResource(R.drawable.ic_refresh)
|
return@setOnClickListener
|
||||||
rotateAnim(it)
|
}
|
||||||
item.downloaded = DownloadStatus.Downloading
|
showMessage("Processing!")
|
||||||
when(source){
|
item.downloaded = DownloadStatus.Queued
|
||||||
Source.YouTube -> {
|
when(source){
|
||||||
viewModel.uiScope.launch {
|
Source.YouTube -> {
|
||||||
YTDownloadHelper.downloadYTTracks(
|
viewModel.viewModelScope.launch {
|
||||||
viewModel.folderType,
|
downloadTracks(arrayListOf(item))
|
||||||
viewModel.subFolder,
|
}
|
||||||
listOf(item)
|
}
|
||||||
)
|
else -> {
|
||||||
}
|
viewModel.viewModelScope.launch {
|
||||||
}
|
downloadTracks(arrayListOf(item))
|
||||||
else -> {
|
}
|
||||||
viewModel.uiScope.launch {
|
|
||||||
DownloadHelper.downloadAllTracks(
|
|
||||||
viewModel.folderType,
|
|
||||||
viewModel.subFolder,
|
|
||||||
listOf(item)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
notifyItemChanged(position)//start showing anim!
|
||||||
}
|
}
|
||||||
notifyItemChanged(position)//start showing anim!
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -17,183 +17,188 @@
|
|||||||
|
|
||||||
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 {
|
||||||
"song" -> {
|
when (type) {
|
||||||
uiScope.launch {
|
"song" -> {
|
||||||
getGaanaSong(link)?.tracks?.firstOrNull()?.also {
|
gaanaInterface.getGaanaSong(seokey = link).value?.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",
|
||||||
name = title.value!!,
|
name = title.value!!,
|
||||||
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" -> {
|
gaanaInterface.getGaanaAlbum(seokey = link).value?.also {
|
||||||
uiScope.launch {
|
|
||||||
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(
|
||||||
type = "Album",
|
DownloadRecord(
|
||||||
name = title.value!!,
|
type = "Album",
|
||||||
link = "https://gaana.com/$type/$link",
|
name = title.value!!,
|
||||||
coverUrl = coverUrl.value.toString(),
|
link = "https://gaana.com/$type/$link",
|
||||||
totalFiles = trackList.value?.size ?: 0,
|
coverUrl = coverUrl.value.toString(),
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
)
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
"playlist" -> {
|
||||||
"playlist" -> {
|
gaanaInterface.getGaanaPlaylist(seokey = link).value?.also {
|
||||||
uiScope.launch {
|
|
||||||
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(
|
||||||
type = "Playlist",
|
DownloadRecord(
|
||||||
name = title.value.toString(),
|
type = "Playlist",
|
||||||
link = "https://gaana.com/$type/$link",
|
name = title.value.toString(),
|
||||||
coverUrl = coverUrl.value.toString(),
|
link = "https://gaana.com/$type/$link",
|
||||||
totalFiles = it.tracks.size,
|
coverUrl = coverUrl.value.toString(),
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
totalFiles = it.tracks.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 =
|
||||||
title.value = it.name
|
gaanaInterface.getGaanaArtistDetails(seokey = link).value?.artist?.firstOrNull()
|
||||||
coverUrl.value = it.artworkLink
|
?.also {
|
||||||
}
|
title.value = it.name
|
||||||
getGaanaArtistTracks(link)?.also {
|
coverUrl.value = it.artworkLink
|
||||||
it.tracks.forEach {track ->
|
}
|
||||||
if(File(finalOutputDir(track.track_title,folderType,subFolder)).exists()){//Download Already Present!!
|
gaanaInterface.getGaanaArtistTracks(seokey = link).value?.also {
|
||||||
|
it.tracks.forEach { track ->
|
||||||
|
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(
|
||||||
type = "Artist",
|
DownloadRecord(
|
||||||
name = artistDetails?.name ?: link,
|
type = "Artist",
|
||||||
link = "https://gaana.com/$type/$link",
|
name = artistDetails?.name ?: link,
|
||||||
coverUrl = coverUrl.value.toString(),
|
link = "https://gaana.com/$type/$link",
|
||||||
totalFiles = trackList.value?.size ?: 0,
|
coverUrl = coverUrl.value.toString(),
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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&_branch_match_id=862039436205270630
|
||||||
Log.i("Spotify Fragment", "$type : $link")
|
* */
|
||||||
|
if (!spotifyLink.contains("open.spotify")) {
|
||||||
|
val resolvedLink = viewModel.resolveLink(spotifyLink)
|
||||||
if(sharedViewModel.spotifyService.value == null){//Authentication pending!!
|
Log.d("Spotify Resolved Link", resolvedLink)
|
||||||
if(isOnline()) mainActivity.authenticateSpotify()
|
spotifyLink = resolvedLink
|
||||||
}
|
|
||||||
|
|
||||||
when{
|
|
||||||
type == "Error" || link == "Error" -> {
|
|
||||||
showMessage("Please Check Your Link!")
|
|
||||||
mainActivity.onBackPressed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||||
if(type == "episode" || type == "show"){//TODO Implementation
|
type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||||
showMessage("Implementing Soon, Stay Tuned!")
|
|
||||||
|
Log.i("Spotify Fragment", "$type : $link")
|
||||||
|
|
||||||
|
if (sharedViewModel.spotifyService.value == null) {//Authentication pending!!
|
||||||
|
if (isOnline()) mainActivity.authenticateSpotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
type == "Error" || link == "Error" -> {
|
||||||
|
showMessage("Please Check Your Link!")
|
||||||
|
mainActivity.onBackPressed()
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
this.viewModel.spotifySearch(type,link)
|
|
||||||
|
|
||||||
binding.btnDownloadAll.setOnClickListener {
|
else -> {
|
||||||
if(!isOnline()){
|
if (type == "episode" || type == "show") {//TODO Implementation
|
||||||
showNoConnectionAlert()
|
showMessage("Implementing Soon, Stay Tuned!")
|
||||||
return@setOnClickListener
|
} else {
|
||||||
}
|
viewModel.spotifySearch(type, link)
|
||||||
binding.btnDownloadAll.visibility = View.GONE
|
|
||||||
binding.downloadingFab.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
rotateAnim(binding.downloadingFab)
|
binding.btnDownloadAll.setOnClickListener {
|
||||||
for (track in this.viewModel.trackList.value ?: listOf()){
|
if (!isOnline()) {
|
||||||
if(track.downloaded != DownloadStatus.Downloaded){
|
showNoConnectionAlert()
|
||||||
track.downloaded = DownloadStatus.Downloading
|
return@setOnClickListener
|
||||||
adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
|
}
|
||||||
|
binding.btnDownloadAll.gone()
|
||||||
|
binding.downloadingFab.apply {
|
||||||
|
visible()
|
||||||
|
rotate()
|
||||||
|
}
|
||||||
|
showMessage("Processing!")
|
||||||
|
sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
loadAllImages(
|
||||||
|
requireActivity(),
|
||||||
|
viewModel.trackList.value?.map { it.albumArtURL },
|
||||||
|
Source.Spotify
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
val finalList = viewModel.trackList.value?.filter{it.downloaded == DownloadStatus.NotDownloaded}
|
||||||
|
if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
|
||||||
|
else downloadTracks(finalList as ArrayList<TrackDetails>)
|
||||||
|
for (track in viewModel.trackList.value ?: listOf()) {
|
||||||
|
if (track.downloaded == DownloadStatus.NotDownloaded) {
|
||||||
|
track.downloaded = DownloadStatus.Queued
|
||||||
|
//adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
showMessage("Processing!")
|
|
||||||
sharedViewModel.uiScope.launch(Dispatchers.Default){
|
|
||||||
val urlList = arrayListOf<String>()
|
|
||||||
this@SpotifyFragment.viewModel.trackList.value?.forEach { urlList.add(it.albumArtURL) }
|
|
||||||
//Appending Source
|
|
||||||
urlList.add("spotify")
|
|
||||||
loadAllImages(
|
|
||||||
requireActivity(),
|
|
||||||
urlList
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.viewModel.uiScope.launch {
|
|
||||||
val finalList = viewModel.trackList.value
|
|
||||||
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
|
|
||||||
DownloadHelper.downloadAllTracks(
|
|
||||||
viewModel.folderType,
|
|
||||||
viewModel.subFolder,
|
|
||||||
finalList ?: listOf(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,167 +17,193 @@
|
|||||||
|
|
||||||
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){
|
||||||
when (type) {
|
viewModelScope.launch {
|
||||||
"track" -> {
|
when (type) {
|
||||||
uiScope.launch {
|
"track" -> {
|
||||||
getTrackDetails(link)?.also {
|
spotifyService?.getTrack(link)?.value?.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) {
|
||||||
type = "Track",
|
databaseDAO.insert(
|
||||||
name = title.value!!,
|
DownloadRecord(
|
||||||
link = "https://open.spotify.com/$type/$link",
|
type = "Track",
|
||||||
coverUrl = coverUrl.value!!,
|
name = title.value!!,
|
||||||
totalFiles = 1,
|
link = "https://open.spotify.com/$type/$link",
|
||||||
downloaded = it.downloaded == DownloadStatus.Downloaded,
|
coverUrl = coverUrl.value!!,
|
||||||
directory = finalOutputDir(it.name,folderType,subFolder)
|
totalFiles = 1,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
"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) {
|
||||||
type = "Album",
|
databaseDAO.insert(
|
||||||
name = title.value!!,
|
DownloadRecord(
|
||||||
link = "https://open.spotify.com/$type/$link",
|
type = "Album",
|
||||||
coverUrl = coverUrl.value.toString(),
|
name = title.value!!,
|
||||||
totalFiles = trackList.value?.size ?: 0,
|
link = "https://open.spotify.com/$type/$link",
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == trackList.value?.size,
|
coverUrl = coverUrl.value.toString(),
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
totalFiles = trackList.value?.size ?: 0,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
"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(
|
||||||
it1.downloaded = DownloadStatus.Downloaded
|
finalOutputDir(
|
||||||
}
|
it1.name!!,
|
||||||
|
folderType,
|
||||||
|
subFolder
|
||||||
|
)
|
||||||
|
).exists()
|
||||||
|
) {//Download Already Present!!
|
||||||
|
it1.downloaded = DownloadStatus.Downloaded
|
||||||
|
}
|
||||||
tempTrackList.add(it1)
|
tempTrackList.add(it1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
type = "Playlist",
|
databaseDAO.insert(
|
||||||
name = title.value.toString(),
|
DownloadRecord(
|
||||||
link = "https://open.spotify.com/$type/$link",
|
type = "Playlist",
|
||||||
coverUrl = coverUrl.value.toString(),
|
name = title.value.toString(),
|
||||||
totalFiles = tempTrackList.size,
|
link = "https://open.spotify.com/$type/$link",
|
||||||
downloaded = File(finalOutputDir(type = folderType,subFolder = subFolder)).listFiles()?.size == tempTrackList.size,
|
coverUrl = coverUrl.value.toString(),
|
||||||
directory = finalOutputDir(type = folderType,subFolder = subFolder)
|
totalFiles = tempTrackList.size,
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"episode" -> {//TODO
|
||||||
|
}
|
||||||
|
"show" -> {//TODO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"episode" -> {//TODO
|
queryActiveTracks()
|
||||||
}
|
|
||||||
"show" -> {//TODO
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,13 +122,23 @@ 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()
|
||||||
)
|
)
|
||||||
title.postValue(
|
title.postValue(
|
||||||
@ -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!")
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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(
|
||||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
trackList: ArrayList<TrackDetails>,
|
||||||
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) }
|
context: Context? = mainActivity
|
||||||
|
) {
|
||||||
|
if(!trackList.isNullOrEmpty()){
|
||||||
|
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||||
|
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
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
@ -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,76 +163,96 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){
|
/**
|
||||||
serviceScope.launch(Dispatchers.IO) {
|
* Function To Download All Tracks Available in a List
|
||||||
for(downloadObj in downloadObjects){
|
**/
|
||||||
try {
|
private fun downloadAllTracks(trackList: List<TrackDetails>) {
|
||||||
val video = ytDownloader.getVideo(downloadObj.ytVideoId)
|
serviceScope.launch(Dispatchers.Default){
|
||||||
val format: Format? = try {
|
trackList.forEach {
|
||||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
}else {
|
||||||
try {
|
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
downloadTrack(it.videoID!!, it)
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
} else {
|
||||||
try {
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
|
||||||
Log.i("YTDownloader", e.toString())
|
object : Callback<String> {
|
||||||
null
|
override fun onResponse(
|
||||||
}
|
call: Call<String>,
|
||||||
}
|
response: Response<String>
|
||||||
}
|
) {
|
||||||
format?.let {
|
val videoId = sortByBestMatch(
|
||||||
val url: String = format.url()
|
getYTTracks(response.body().toString()),
|
||||||
Log.i("DHelper Link Found", url)
|
trackName = it.title,
|
||||||
serviceScope.launch {
|
trackArtists = it.artists,
|
||||||
val request= Request(url, downloadObj.outputFile)
|
trackDurationSec = it.durationSec
|
||||||
request.priority = Priority.NORMAL
|
).keys.firstOrNull()
|
||||||
request.networkType = NetworkType.ALL
|
Log.i("Service VideoID", videoId ?: "Not Found")
|
||||||
|
if (videoId.isNullOrBlank()) sendTrackBroadcast(
|
||||||
|
Status.FAILED.name,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
else {//Found Youtube Video ID
|
||||||
|
downloadTrack(videoId, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fetch.enqueue(request,
|
override fun onFailure(call: Call<String>, t: Throwable) {
|
||||||
{
|
if (t.message.toString()
|
||||||
requestMap[it] = downloadObj.trackDetails
|
.contains("Failed to connect")
|
||||||
Log.i(tag, "Enqueuing Download")
|
) showMessage("Failed, Check Your Internet Connection!")
|
||||||
},
|
Log.i("YT API Req. Fail", t.message.toString())
|
||||||
{
|
}
|
||||||
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
|
||||||
Log.i("Service YT Error", e.message.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
fun downloadTrack(videoID:String,track: TrackDetails){
|
||||||
super.onDestroy()
|
serviceScope.launch(Dispatchers.IO) {
|
||||||
if(converted == total){
|
try {
|
||||||
Handler().postDelayed({
|
val video = ytDownloader.getVideo(videoID)
|
||||||
Log.i(tag,"Service destroyed.")
|
val format: Format? = try {
|
||||||
deleteFile(parentDirectory)
|
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
releaseWakeLock()
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
stopForeground(true)
|
try {
|
||||||
},2000)
|
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||||
}
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
}
|
try {
|
||||||
|
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
super.onTaskRemoved(rootIntent)
|
Log.i("YTDownloader", e.toString())
|
||||||
if(converted == total ){
|
null
|
||||||
Log.i(tag,"Service Removed.")
|
}
|
||||||
deleteFile(parentDirectory)
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
}
|
||||||
stopForeground(true)
|
format?.let {
|
||||||
} else {
|
val url: String = format.url()
|
||||||
stopSelf()//System will automatically close it
|
Log.i("DHelper Link Found", url)
|
||||||
|
val request= Request(url, track.outputFile).apply{
|
||||||
|
priority = Priority.NORMAL
|
||||||
|
networkType = NetworkType.ALL
|
||||||
|
}
|
||||||
|
fetch.enqueue(request,
|
||||||
|
{
|
||||||
|
requestMap[it] = track
|
||||||
|
Log.i(tag, "Enqueuing Download")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}catch (e: com.github.kiulian.downloader.YoutubeException){
|
||||||
|
Log.i("Service YT Error", e.message.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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")
|
||||||
updateNotification()
|
track?.let{
|
||||||
|
allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(it.title)] =
|
||||||
|
it.apply { downloaded = DownloadStatus.Downloading }
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
28
app/src/main/res/drawable/circular_background.xml
Normal file
28
app/src/main/res/drawable/circular_background.xml
Normal 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>
|
||||||
|
|
||||||
|
|
@ -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"/>
|
||||||
|
27
app/src/main/res/drawable/ic_baseline_cancel_24.xml
Normal file
27
app/src/main/res/drawable/ic_baseline_cancel_24.xml
Normal 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>
|
32
app/src/main/res/drawable/ic_error.xml
Normal file
32
app/src/main/res/drawable/ic_error.xml
Normal 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>
|
61
app/src/main/res/drawable/progress_bar.xml
Normal file
61
app/src/main/res/drawable/progress_bar.xml
Normal 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>
|
133
app/src/main/res/layout/download_song_item.xml
Normal file
133
app/src/main/res/layout/download_song_item.xml
Normal 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>
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
14
build.gradle
14
build.gradle
@ -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'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'))
|
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user