mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Latest FFMPEG Async Tasks,Mp3 Art and Album Data,Fetch Downloader Implemented.
This commit is contained in:
parent
ac683279e6
commit
a802751942
@ -1,7 +1,10 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="shabinder">
|
||||
<words>
|
||||
<w>ffmpeg</w>
|
||||
<w>flyer</w>
|
||||
<w>insta</w>
|
||||
<w>instagram</w>
|
||||
<w>moshi</w>
|
||||
<w>musicforeveryone</w>
|
||||
<w>musicplaceholder</w>
|
||||
|
@ -31,5 +31,10 @@
|
||||
<option name="name" value="maven" />
|
||||
<option name="url" value="https://jitpack.io" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="maven2" />
|
||||
<option name="name" value="maven2" />
|
||||
<option name="url" value="https://dl.bintray.com/hummatli/maven/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
@ -20,7 +20,7 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
@ -28,16 +28,14 @@ android {
|
||||
|
||||
buildFeatures{
|
||||
dataBinding = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.shabinder.spotiflyer'
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
versionCode 2
|
||||
versionName "1.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@ -51,15 +49,20 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
@ -72,17 +75,21 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
|
||||
|
||||
// implementation "androidx.room:room-runtime:2.2.5"
|
||||
// kapt "androidx.room:room-compiler:2.2.5"
|
||||
// implementation "androidx.room:room-ktx:2.2.5"
|
||||
implementation "com.github.bumptech.glide:glide:4.11.0"
|
||||
kapt "com.github.bumptech.glide:compiler:4.11.0"
|
||||
implementation "androidx.room:room-runtime:2.2.5"
|
||||
kapt "androidx.room:room-compiler:2.2.5"
|
||||
implementation "androidx.room:room-ktx:2.2.5"
|
||||
implementation ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||
transitive = true
|
||||
}
|
||||
kapt ("com.github.bumptech.glide:recyclerview-integration:4.11.0") {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
|
||||
implementation 'com.google.apis:google-api-services-youtube:v3-rev180-1.22.0'
|
||||
implementation 'com.google.oauth-client:google-oauth-client:1.22.0'
|
||||
implementation 'com.spotify.android:auth:1.1.0'
|
||||
// implementation 'com.spotify.android:auth:1.1.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.8.0'
|
||||
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
@ -90,8 +97,16 @@ dependencies {
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:2.9.0"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // or "kotlin-stdlib-jdk8"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" // JVM dependency
|
||||
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'com.arthenica:mobile-ffmpeg-audio:4.4.LTS'
|
||||
|
||||
implementation 'com.shreyaspatil:EasyUpiPayment:2.2'
|
||||
implementation 'com.github.sealedtx:java-youtube-downloader:2.2.2'
|
||||
implementation 'com.github.sealedtx:java-youtube-downloader:2.2.3'
|
||||
implementation "androidx.tonyodev.fetch2:xfetch2:3.1.4"
|
||||
implementation 'com.github.javiersantos:AppUpdater:2.7'
|
||||
|
||||
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
|
@ -26,9 +26,12 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />-->
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
@ -48,20 +51,21 @@
|
||||
<activity android:name="com.shabinder.spotiflyer.splash.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Transparent">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
<!-- <activity
|
||||
android:name="com.spotify.sdk.android.authentication.LoginActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
-->
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
android:resource="@array/preloaded_fonts" />
|
||||
|
||||
<service android:name=".worker.ForegroundService"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
63
app/src/main/java/com/shabinder/spotiflyer/App.kt
Normal file
63
app/src/main/java/com/shabinder/spotiflyer/App.kt
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
class App:Application() {
|
||||
private val channelId = "ForegroundServiceChannel"
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
channelId,
|
||||
"ForeGround Service Channel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -18,23 +18,26 @@
|
||||
package com.shabinder.spotiflyer
|
||||
|
||||
import android.Manifest
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.github.javiersantos.appupdater.AppUpdater
|
||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.spotiflyer.databinding.MainActivityBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.utils.SpotifyServiceToken
|
||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
||||
import com.shabinder.spotiflyer.utils.createDirectory
|
||||
import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
@ -48,12 +51,11 @@ import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class MainActivity : AppCompatActivity() ,DownloadHelper{
|
||||
class MainActivity : AppCompatActivity(){
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private var ytDownloader : YoutubeDownloader? = null
|
||||
private var spotifyService : SpotifyService? = null
|
||||
private var spotifyServiceToken : SpotifyServiceToken? = null
|
||||
private var downloadManager : DownloadManager? = null
|
||||
// private val redirectUri = "spotiflyer://callback"
|
||||
private val clientId:String = "694d8bf4f6ec420fa66ea7fb4c68f89d"
|
||||
private val clientSecret:String = "02ca2d4021a7452dae2328b47a6e8fe8"
|
||||
@ -63,20 +65,21 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
|
||||
private var token :String =""
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = DataBindingUtil.setContentView(this,R.layout.main_activity)
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
|
||||
sharedPref = this.getPreferences(Context.MODE_PRIVATE)
|
||||
|
||||
// if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){
|
||||
// val savedToken = sharedPref?.getString("token","error")!!
|
||||
// sharedViewModel.accessToken.value = savedToken
|
||||
// Log.i("SharedPrefs Token:",savedToken)
|
||||
// token = savedToken
|
||||
//
|
||||
// implementSpotifyService(savedToken)
|
||||
// }else{authenticateSpotify()}
|
||||
/* if(sharedPref?.contains("token")!! && (sharedPref?.getLong("time",System.currentTimeMillis()/1000/60/60)!! < (System.currentTimeMillis()/1000/60/60)) ){
|
||||
val savedToken = sharedPref?.getString("token","error")!!
|
||||
sharedViewModel.accessToken.value = savedToken
|
||||
Log.i("SharedPrefs Token:",savedToken)
|
||||
token = savedToken
|
||||
|
||||
implementSpotifyService(savedToken)
|
||||
}else{authenticateSpotify()}*/
|
||||
|
||||
if(sharedViewModel.spotifyService == null){
|
||||
authenticateSpotify()
|
||||
@ -85,6 +88,12 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
|
||||
}
|
||||
|
||||
requestPermission()
|
||||
checkIfLatestVersion()
|
||||
createDir()
|
||||
setUpi()
|
||||
isConnected = isOnline()
|
||||
sharedViewModel.isConnected.value = isConnected
|
||||
Log.i("Connection Status",isConnected.toString())
|
||||
|
||||
//Object to download From Youtube {"https://github.com/sealedtx/java-youtube-downloader"}
|
||||
ytDownloader = YoutubeDownloader()
|
||||
@ -92,32 +101,9 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
|
||||
//Initialing Communication with Youtube
|
||||
YoutubeInterface.youtubeConnector()
|
||||
|
||||
//Getting System Download Manager
|
||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
sharedViewModel.downloadManager = downloadManager
|
||||
|
||||
isConnected = isOnline()
|
||||
sharedViewModel.isConnected.value = isConnected
|
||||
|
||||
Log.i("Connection Status",isConnected.toString())
|
||||
|
||||
|
||||
easyUpiPayment = EasyUpiPayment.Builder()
|
||||
.with(this)
|
||||
.setPayeeVpa("technoshab@paytm")
|
||||
.setPayeeName("Shabinder Singh")
|
||||
.setTransactionId("UNIQUE_TRANSACTION_ID")
|
||||
.setTransactionRefId("UNIQUE_TRANSACTION_REF_ID")
|
||||
.setDescription("Thanks for donating")
|
||||
.setAmount("39.00")
|
||||
.build()
|
||||
|
||||
sharedViewModel.easyUpiPayment = easyUpiPayment
|
||||
|
||||
handleIntentFromExternalActivity()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adding my own new Spotify Web Api Requests!
|
||||
* */
|
||||
@ -246,6 +232,48 @@ class MainActivity : AppCompatActivity() ,DownloadHelper{
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpi() {
|
||||
easyUpiPayment = EasyUpiPayment.Builder()
|
||||
.with(this)
|
||||
.setPayeeVpa("technoshab@paytm")
|
||||
.setPayeeName("Shabinder Singh")
|
||||
.setTransactionId("UNIQUE_TRANSACTION_ID")
|
||||
.setTransactionRefId("UNIQUE_TRANSACTION_REF_ID")
|
||||
.setDescription("Thanks for donating")
|
||||
.setAmount("39.00")
|
||||
.build()
|
||||
|
||||
sharedViewModel.easyUpiPayment = easyUpiPayment
|
||||
|
||||
}
|
||||
|
||||
private fun createDir() {
|
||||
createDirectory(DownloadHelper.defaultDir)
|
||||
createDirectory(DownloadHelper.defaultDir+".Images/")
|
||||
createDirectory(DownloadHelper.defaultDir+"Tracks/")
|
||||
createDirectory(DownloadHelper.defaultDir+"Albums/")
|
||||
createDirectory(DownloadHelper.defaultDir+"Playlists/")
|
||||
}
|
||||
|
||||
private fun checkIfLatestVersion() {
|
||||
val appUpdater = AppUpdater(this)
|
||||
.showAppUpdated(false)//true:Show App is Update Dialog
|
||||
.setUpdateFrom(UpdateFrom.XML)
|
||||
.setUpdateXML("https://raw.githubusercontent.com/Shabinder/SpotiFlyer/master/app/src/main/res/xml/app_update.xml")
|
||||
.setCancelable(false)
|
||||
.setButtonUpdateClickListener { _, _ ->
|
||||
val uri: Uri =
|
||||
Uri.parse("http://github.com/Shabinder/SpotiFlyer/releases")
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setButtonDismissClickListener { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
appUpdater.start()
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
private fun authenticateSpotify() {
|
||||
val builder = AuthenticationRequest.Builder(clientId,AuthenticationResponse.Type.TOKEN,redirectUri)
|
||||
|
@ -17,9 +17,9 @@
|
||||
|
||||
package com.shabinder.spotiflyer
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Environment
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
@ -32,15 +32,16 @@ import com.shreyaspatil.EasyUpiPayment.EasyUpiPayment
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import java.io.File
|
||||
|
||||
class SharedViewModel : ViewModel() {
|
||||
var intentString = ""
|
||||
var accessToken = MutableLiveData<String>().apply { value = "" }
|
||||
var spotifyService : SpotifyService? = null
|
||||
var ytDownloader : YoutubeDownloader? = null
|
||||
var downloadManager : DownloadManager? = null
|
||||
var isConnected = MutableLiveData<Boolean>().apply { value = false }
|
||||
var easyUpiPayment: EasyUpiPayment? = null
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator + ".Images" + File.separator
|
||||
|
||||
|
||||
private var viewModelJob = Job()
|
||||
@ -64,13 +65,12 @@ class SharedViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun showAlertDialog(resources:Resources,context: Context){
|
||||
val dialog = MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
|
||||
MaterialAlertDialogBuilder(context,R.style.AlertDialogTheme)
|
||||
.setTitle(resources.getString(R.string.title))
|
||||
.setMessage(resources.getString(R.string.supporting_text))
|
||||
.setPositiveButton(resources.getString(R.string.cancel)) { _, _ ->
|
||||
// Respond to neutral button press
|
||||
}
|
||||
.setBackground(resources.getDrawable(R.drawable.gradient))
|
||||
.show()
|
||||
}
|
||||
}
|
@ -17,22 +17,28 @@
|
||||
|
||||
package com.shabinder.spotiflyer.downloadHelper
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||
import android.net.Uri
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
import com.shabinder.spotiflyer.fragments.MainFragment
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.utils.YoutubeInterface
|
||||
import com.shabinder.spotiflyer.worker.ForegroundService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
interface DownloadHelper {
|
||||
object DownloadHelper {
|
||||
|
||||
var context : Context? = null
|
||||
val defaultDir = Environment.DIRECTORY_MUSIC + File.separator + "SpotiFlyer" + File.separator
|
||||
private var downloadList = arrayListOf<DownloadObject>()
|
||||
|
||||
/**
|
||||
* Function To Download All Tracks Available in a List
|
||||
@ -40,111 +46,106 @@ interface DownloadHelper {
|
||||
suspend fun downloadAllTracks(
|
||||
type:String,
|
||||
subFolder: String?,
|
||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?, downloadManager: DownloadManager?) {
|
||||
trackList.forEach { downloadTrack(null,type,subFolder,ytDownloader,downloadManager,"${it.name} ${it.artists?.get(0)?.name ?:""}") }
|
||||
trackList: List<Track>, ytDownloader: YoutubeDownloader?) {
|
||||
var size = trackList.size
|
||||
trackList.forEach {
|
||||
size--
|
||||
if(size == 0){
|
||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it ,0 )
|
||||
}else{
|
||||
downloadTrack(null,type,subFolder,ytDownloader,"${it.name} ${it.artists?.get(0)?.name ?:""}", it )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun downloadTrack(
|
||||
mainFragment: MainFragment?,
|
||||
type:String,
|
||||
subFolder:String?,
|
||||
ytDownloader: YoutubeDownloader?,
|
||||
downloadManager: DownloadManager?,
|
||||
searchQuery: String
|
||||
searchQuery: String,
|
||||
track: Track,
|
||||
index: Int? = null
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val data = YoutubeInterface.search(searchQuery)?.get(0)
|
||||
if (data == null) {
|
||||
Log.i("DownloadHelper", "Youtube Request Failed!")
|
||||
} else {
|
||||
val data: YoutubeInterface.VideoItem = YoutubeInterface.search(searchQuery)?.get(0)!!
|
||||
|
||||
val video = ytDownloader?.getVideo(data.id)
|
||||
//Fetching a Video Object.
|
||||
val details = video?.details()
|
||||
try{
|
||||
try {
|
||||
val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data)
|
||||
withContext(Dispatchers.Main) {
|
||||
mainFragment?.showToast("Starting Download")
|
||||
}
|
||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
try {
|
||||
val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data)
|
||||
withContext(Dispatchers.Main) {
|
||||
mainFragment?.showToast("Starting Download")
|
||||
}
|
||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
try {
|
||||
val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data)
|
||||
withContext(Dispatchers.Main) {
|
||||
mainFragment?.showToast("Starting Download")
|
||||
}
|
||||
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
Log.i("Catch", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String {
|
||||
val video = ytDownloader?.getVideo(data.id)
|
||||
val format: Format =
|
||||
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||
val audioUrl = format.url()
|
||||
video?.findAudioWithQuality(quality)?.get(0) as Format
|
||||
Log.i("Format", video.findAudioWithQuality(AudioQuality.medium)?.get(0)!!.mimeType())
|
||||
val audioUrl:String = format.url()
|
||||
Log.i("DHelper Link Found", audioUrl)
|
||||
if (audioUrl != null) {
|
||||
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
|
||||
withContext(Dispatchers.Main){
|
||||
mainFragment?.showToast("Download Started")
|
||||
}
|
||||
} else {
|
||||
Log.i("YT audio url is null", format.toString())
|
||||
}
|
||||
}catch (e:ArrayIndexOutOfBoundsException){
|
||||
try{
|
||||
val format: Format =
|
||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||
val audioUrl = format.url()
|
||||
Log.i("DHelper Link Found", audioUrl)
|
||||
if (audioUrl != null) {
|
||||
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
|
||||
withContext(Dispatchers.Main){
|
||||
mainFragment?.showToast("Download Started")
|
||||
}
|
||||
} else {
|
||||
Log.i("YT audio url is null", format.toString())
|
||||
}
|
||||
}catch (e:ArrayIndexOutOfBoundsException){
|
||||
try{
|
||||
val format: Format =
|
||||
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||
val audioUrl = format.url()
|
||||
Log.i("DHelper Link Found", audioUrl)
|
||||
if (audioUrl != null) {
|
||||
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
|
||||
withContext(Dispatchers.Main){
|
||||
mainFragment?.showToast("Download Started")
|
||||
}
|
||||
} else {
|
||||
Log.i("YT audio url is null", format.toString())
|
||||
}
|
||||
}catch(e:ArrayIndexOutOfBoundsException){
|
||||
Log.i("Catch",e.toString())
|
||||
}
|
||||
}
|
||||
return audioUrl
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloading Using Android Download Manager
|
||||
* */
|
||||
suspend fun downloadFile(url: String, downloadManager: DownloadManager?, title: String,subFolder: String?,type: String) {
|
||||
private suspend fun downloadFile(url: String, title: String, subFolder: String?, type: String, track:Track, index:Int? = null,mainFragment: MainFragment?) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val audioUri = Uri.parse(url)
|
||||
val outputDir:String =
|
||||
File.separator + "SpotiFlyer" + File.separator + type + File.separator + (if(subFolder == null){""}else{subFolder + File.separator}) + "${removeIllegalChars(title)}.mp3"
|
||||
val outputFile:String = Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
DownloadHelper.defaultDir + removeIllegalChars(type) + File.separator + (if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} + removeIllegalChars(track.name!!)+".m4a")
|
||||
|
||||
val request = DownloadManager.Request(audioUri)
|
||||
.setAllowedNetworkTypes(
|
||||
DownloadManager.Request.NETWORK_WIFI or
|
||||
DownloadManager.Request.NETWORK_MOBILE
|
||||
if(!File(removeIllegalChars(outputFile.substringBeforeLast('.')) +".mp3").exists()){
|
||||
val downloadObject = DownloadObject(
|
||||
track = track,
|
||||
url = url,
|
||||
outputDir = outputFile
|
||||
)
|
||||
.setAllowedOverRoaming(false)
|
||||
.setTitle(title)
|
||||
.setDescription("Spotify Downloader Working Up here...")
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, outputDir)
|
||||
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
downloadManager?.enqueue(request)
|
||||
Log.i("DownloadManager", "Download Request Sent")
|
||||
|
||||
Log.i("DH",outputFile)
|
||||
if(index==null){
|
||||
downloadList.add(downloadObject)
|
||||
}else{
|
||||
downloadList.add(downloadObject)
|
||||
startService(context!!, downloadList)
|
||||
downloadList = arrayListOf()
|
||||
}
|
||||
}else{withContext(Dispatchers.Main){mainFragment?.showToast("${track.name} is already Downloaded")}}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun startService(context:Context,list: ArrayList<DownloadObject>) {
|
||||
val serviceIntent = Intent(context, ForegroundService::class.java)
|
||||
serviceIntent.putParcelableArrayListExtra("list",list)
|
||||
ContextCompat.startForegroundService(context, serviceIntent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removing Illegal Chars from File Name
|
||||
* **/
|
||||
fun removeIllegalChars(fileName: String): String? {
|
||||
private fun removeIllegalChars(fileName: String): String? {
|
||||
val illegalCharArray = charArrayOf(
|
||||
'/',
|
||||
'\n',
|
||||
@ -161,12 +162,14 @@ interface DownloadHelper {
|
||||
'|',
|
||||
'\"',
|
||||
'.',
|
||||
':'
|
||||
':',
|
||||
'-'
|
||||
)
|
||||
var name = fileName
|
||||
for (c in illegalCharArray) {
|
||||
name = fileName.replace(c, '_')
|
||||
}
|
||||
name = name.replace("\\s".toRegex(), "_")
|
||||
return name
|
||||
}
|
||||
}
|
@ -23,31 +23,42 @@ import android.content.pm.PackageManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.databinding.MainFragmentBinding
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadAllTracks
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
|
||||
import com.shabinder.spotiflyer.utils.SpotifyService
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import com.shabinder.spotiflyer.utils.copyTo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class MainFragment : Fragment(),DownloadHelper {
|
||||
class MainFragment : Fragment() {
|
||||
private lateinit var binding:MainFragmentBinding
|
||||
private lateinit var mainViewModel: MainViewModel
|
||||
private lateinit var sharedViewModel: SharedViewModel
|
||||
@ -56,12 +67,13 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
private var type:String = ""
|
||||
private var spotifyLink = ""
|
||||
private var i: Intent? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(inflater,R.layout.main_fragment,container,false)
|
||||
|
||||
DownloadHelper.context = requireContext()
|
||||
sharedViewModel = ViewModelProvider(this.requireActivity()).get(SharedViewModel::class.java)
|
||||
mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
|
||||
spotifyService = sharedViewModel.spotifyService
|
||||
@ -73,13 +85,14 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
|
||||
binding.usage.text = spanStringBuilder
|
||||
openSpotifyButton()
|
||||
openGithubButton()
|
||||
openInstaButton()
|
||||
|
||||
binding.btnDonate.setOnClickListener {
|
||||
sharedViewModel.easyUpiPayment?.startPayment()
|
||||
}
|
||||
|
||||
binding.btnSearch.setOnClickListener {
|
||||
sharedViewModel.isConnected.value = isOnline()
|
||||
spotifyLink = binding.linkSearch.text.toString()
|
||||
|
||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||
@ -87,16 +100,15 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
|
||||
Log.i("Fragment", "$type : $link")
|
||||
|
||||
if(sharedViewModel.spotifyService == null){
|
||||
if(sharedViewModel.spotifyService == null && !isOnline()){
|
||||
(activity as MainActivity).authenticateSpotify()
|
||||
}
|
||||
|
||||
if (type == "Error" || link == "Error") {
|
||||
showToast("Please Check Your Link!")
|
||||
} else if(sharedViewModel.isConnected.value == false){
|
||||
} else if(!isOnline()){
|
||||
sharedViewModel.showAlertDialog(resources,requireContext())
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
adapter = TrackListAdapter()
|
||||
binding.trackList.adapter = adapter
|
||||
adapter.sharedViewModel = sharedViewModel
|
||||
@ -106,7 +118,9 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
if(mainViewModel.searchLink == spotifyLink){
|
||||
//it's a Device Configuration Change
|
||||
adapterConfig(mainViewModel.trackList)
|
||||
sharedViewModel.uiScope.launch {
|
||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||
}
|
||||
}else{
|
||||
when (type) {
|
||||
"track" -> {
|
||||
@ -121,15 +135,14 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
adapterConfig(trackList)
|
||||
|
||||
binding.btnDownloadAll.setOnClickListener {
|
||||
showToast("Starting Download in Few Seconds")
|
||||
sharedViewModel.uiScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadAllTracks(
|
||||
"Tracks",
|
||||
null,
|
||||
trackList,
|
||||
sharedViewModel.ytDownloader,
|
||||
sharedViewModel.downloadManager
|
||||
)
|
||||
sharedViewModel.ytDownloader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,22 +155,21 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
sharedViewModel.uiScope.launch {
|
||||
val albumObject = sharedViewModel.getAlbumDetails(link)
|
||||
val trackList = mutableListOf<Track>()
|
||||
albumObject!!.tracks?.items?.forEach { trackList.add(it!!) }
|
||||
albumObject!!.tracks?.items?.forEach { trackList.add(it) }
|
||||
mainViewModel.trackList = trackList
|
||||
mainViewModel.coverUrl = albumObject.images?.get(0)!!.url!!
|
||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||
adapter.isAlbum = true
|
||||
adapterConfig(trackList)
|
||||
binding.btnDownloadAll.setOnClickListener {
|
||||
showToast("Starting Download in Few Seconds")
|
||||
sharedViewModel.uiScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadAllTracks(
|
||||
"Albums",
|
||||
albumObject.name,
|
||||
trackList,
|
||||
sharedViewModel.ytDownloader,
|
||||
sharedViewModel.downloadManager
|
||||
)
|
||||
sharedViewModel.ytDownloader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,21 +183,21 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
sharedViewModel.uiScope.launch {
|
||||
val playlistObject = sharedViewModel.getPlaylistDetails(link)
|
||||
val trackList = mutableListOf<Track>()
|
||||
playlistObject!!.tracks?.items!!.forEach { trackList.add(it?.track!!) }
|
||||
playlistObject!!.tracks?.items!!.forEach { trackList.add(it.track!!) }
|
||||
mainViewModel.trackList = trackList
|
||||
mainViewModel.coverUrl = playlistObject.images?.get(0)!!.url!!
|
||||
bindImage(binding.imageView,mainViewModel.coverUrl)
|
||||
adapterConfig(trackList)
|
||||
binding.btnDownloadAll.setOnClickListener {
|
||||
showToast("Starting Download in Few Seconds")
|
||||
sharedViewModel.uiScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
loadAllImages(trackList)
|
||||
downloadAllTracks(
|
||||
"Playlists",
|
||||
playlistObject.name,
|
||||
trackList,
|
||||
sharedViewModel.ytDownloader,
|
||||
sharedViewModel.downloadManager
|
||||
)
|
||||
sharedViewModel.ytDownloader)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,6 +225,59 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to fetch all Images for using in mp3 tag.
|
||||
**/
|
||||
private fun loadAllImages(trackList: List<Track>) {
|
||||
trackList.forEach {
|
||||
val imgUrl = it.album!!.images?.get(0)?.url
|
||||
imgUrl?.let {
|
||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asFile()
|
||||
.load(imgUri)
|
||||
.listener(object: RequestListener<File> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<File>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
Log.i("Glide","LoadFailed")
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: File?,
|
||||
model: Any?,
|
||||
target: Target<File>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
sharedViewModel.uiScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
try {
|
||||
val file = File(
|
||||
Environment.getExternalStorageDirectory(),
|
||||
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
|
||||
)
|
||||
resource?.copyTo(file)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}).submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementing button to Open Spotify App
|
||||
**/
|
||||
private fun openSpotifyButton() {
|
||||
val manager: PackageManager = requireActivity().packageManager
|
||||
try {
|
||||
@ -232,14 +297,32 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openGithubButton() {
|
||||
val uri: Uri =
|
||||
Uri.parse("http://github.com/Shabinder/SpotiFlyer")
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
binding.btnGithub.setOnClickListener {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
private fun openInstaButton() {
|
||||
val uri: Uri =
|
||||
Uri.parse("http://www.instagram.com/mr.shabinder")
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
binding.developerInsta.setOnClickListener {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure Recycler View Adapter
|
||||
**/
|
||||
private fun adapterConfig(trackList: List<Track>){
|
||||
adapter.trackList = trackList.toList()
|
||||
adapter.totalItems = trackList.size
|
||||
adapter.mainFragment = this
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -274,6 +357,10 @@ class MainFragment : Fragment(),DownloadHelper {
|
||||
fun showToast(message:String){
|
||||
Toast.makeText(context,message,Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Util. Function To Check Connection Status
|
||||
**/
|
||||
private fun isOnline(): Boolean {
|
||||
val cm =
|
||||
requireActivity().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
@ -21,7 +21,7 @@ import androidx.lifecycle.ViewModel
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
|
||||
class MainViewModel: ViewModel() {
|
||||
var searchLink:String = ""
|
||||
var searchLink: String = ""
|
||||
var trackList = mutableListOf<Track>()
|
||||
var coverUrl:String = ""
|
||||
var coverUrl: String = ""
|
||||
}
|
@ -17,6 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Album(
|
||||
var album_type: String? = null,
|
||||
var artists: List<Artist?>? = null,
|
||||
@ -33,6 +37,6 @@ data class Album(
|
||||
var popularity: Int? = null,
|
||||
var release_date: String? = null,
|
||||
var release_date_precision: String? = null,
|
||||
var tracks: PagingObject<Track?>? = null,
|
||||
var tracks: PagingObjectTrack? = null,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null):Parcelable
|
@ -16,10 +16,15 @@
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Artist(
|
||||
var external_urls: Map<String?, String?>? = null,
|
||||
var href: String? = null,
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null):Parcelable
|
@ -16,6 +16,11 @@
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Copyright(
|
||||
var text: String? = null,
|
||||
var type: String? = null)
|
||||
var type: String? = null):Parcelable
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Shabinder Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class DownloadObject(
|
||||
var track: Track,
|
||||
var url:String,
|
||||
var outputDir:String
|
||||
):Parcelable
|
@ -17,6 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Episodes(
|
||||
var audio_preview_url:String?,
|
||||
var description:String?,
|
||||
@ -35,4 +39,4 @@ data class Episodes(
|
||||
var release_date_precision:String?,
|
||||
var type:String?,
|
||||
var uri:String
|
||||
)
|
||||
): Parcelable
|
@ -17,6 +17,9 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Followers(
|
||||
var href: String? = null,
|
||||
var total: Int? = null)
|
||||
var total: Int? = null):java.io.Serializable
|
@ -16,7 +16,12 @@
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
var width: Int? = null,
|
||||
var height: Int? = null,
|
||||
var url: String? = null)
|
||||
var url: String? = null):Parcelable
|
@ -17,9 +17,13 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class LinkedTrack(
|
||||
var external_urls: Map<String?, String?>? = null,
|
||||
var href: String? = null,
|
||||
var id: String? = null,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null): Parcelable
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Shabinder Singh
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class PagingObjectPlaylistTrack(
|
||||
var href: String? = null,
|
||||
var items: List<PlaylistTrack>? = null,
|
||||
var limit: Int = 0,
|
||||
var next: String? = null,
|
||||
var offset: Int = 0,
|
||||
var previous: String? = null,
|
||||
var total: Int = 0): Parcelable
|
@ -17,11 +17,15 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
data class PagingObject<T>(
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class PagingObjectTrack(
|
||||
var href: String? = null,
|
||||
var items: List<T>? = null,
|
||||
var items: List<Track>? = null,
|
||||
var limit: Int = 0,
|
||||
var next: String? = null,
|
||||
var offset: Int = 0,
|
||||
var previous: String? = null,
|
||||
var total: Int = 0)
|
||||
var total: Int = 0):Parcelable
|
@ -17,7 +17,11 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.Json
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Playlist(
|
||||
@Json(name = "collaborative")var is_collaborative: Boolean? = null,
|
||||
var description: String? = null,
|
||||
@ -30,6 +34,6 @@ data class Playlist(
|
||||
var owner: UserPublic? = null,
|
||||
@Json(name = "public")var is_public: Boolean? = null,
|
||||
var snapshot_id: String? = null,
|
||||
var tracks: PagingObject<PlaylistTrack?>? = null,
|
||||
var tracks: PagingObjectPlaylistTrack? = null,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null): Parcelable
|
@ -17,8 +17,12 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class PlaylistTrack(
|
||||
var added_at: String? = null,
|
||||
var added_by: UserPublic? = null,
|
||||
var track: Track? = null,
|
||||
var is_local: Boolean? = null)
|
||||
var is_local: Boolean? = null): Parcelable
|
@ -17,8 +17,12 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Token(
|
||||
var access_token:String,
|
||||
var token_type:String,
|
||||
var expires_in:Int
|
||||
)
|
||||
): Parcelable
|
@ -17,6 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class Track(
|
||||
var artists: List<Artist?>? = null,
|
||||
var available_markets: List<String?>? = null,
|
||||
@ -35,4 +39,4 @@ data class Track(
|
||||
var uri: String? = null,
|
||||
var album: Album? = null,
|
||||
var external_ids: Map<String?, String?>? = null,
|
||||
var popularity: Int? = null)
|
||||
var popularity: Int? = null):Parcelable
|
@ -17,6 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class UserPrivate(
|
||||
val country:String,
|
||||
var display_name: String,
|
||||
@ -28,4 +32,4 @@ data class UserPrivate(
|
||||
var images: List<Image?>? = null,
|
||||
var product:String,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null): Parcelable
|
@ -17,6 +17,10 @@
|
||||
|
||||
package com.shabinder.spotiflyer.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class UserPublic(
|
||||
var display_name: String? = null,
|
||||
var external_urls: Map<String?, String?>? = null,
|
||||
@ -25,4 +29,4 @@ data class UserPublic(
|
||||
var id: String? = null,
|
||||
var images: List<Image?>? = null,
|
||||
var type: String? = null,
|
||||
var uri: String? = null)
|
||||
var uri: String? = null): Parcelable
|
@ -26,13 +26,14 @@ import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.SharedViewModel
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper.downloadTrack
|
||||
import com.shabinder.spotiflyer.fragments.MainFragment
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.shabinder.spotiflyer.utils.bindImage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>(),DownloadHelper {
|
||||
|
||||
class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>() {
|
||||
|
||||
var trackList = listOf<Track>()
|
||||
var totalItems:Int = 0
|
||||
@ -52,15 +53,17 @@ class TrackListAdapter:RecyclerView.Adapter<TrackListAdapter.ViewHolder>(),Downl
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = trackList[position]
|
||||
if(totalItems == 1 || isAlbum){holder.coverImage.visibility = View.GONE}else{
|
||||
sharedViewModel.uiScope.launch {
|
||||
bindImage(holder.coverImage, item.album!!.images?.get(0)?.url)
|
||||
}
|
||||
}
|
||||
|
||||
holder.trackName.text = "${if(item.name!!.length > 17){"${item.name!!.subSequence(0,16)}..."}else{item.name}}"
|
||||
holder.artistName.text = "${item.artists?.get(0)?.name?:""}..."
|
||||
holder.duration.text = "${item.duration_ms/1000/60} minutes, ${(item.duration_ms/1000)%60} sec"
|
||||
holder.downloadBtn.setOnClickListener{
|
||||
sharedViewModel.uiScope.launch {
|
||||
downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,sharedViewModel.downloadManager,"${item.name} ${item.artists?.get(0)!!.name?:""}")
|
||||
downloadTrack(mainFragment,"Tracks",null,sharedViewModel.ytDownloader,"${item.name} ${item.artists?.get(0)!!.name?:""}",track = item,index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,22 +17,102 @@
|
||||
|
||||
package com.shabinder.spotiflyer.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.widget.ImageView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
@BindingAdapter("imageUrl")
|
||||
fun bindImage(imgView: ImageView, imgUrl: String?) {
|
||||
imgUrl?.let {
|
||||
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
|
||||
Glide.with(imgView.context)
|
||||
Glide
|
||||
.with(imgView.context)
|
||||
.asFile()
|
||||
.load(imgUri)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.drawable.ic_song_placeholder)
|
||||
.error(R.drawable.ic_musicplaceholder))
|
||||
.into(imgView)
|
||||
.error(R.drawable.ic_musicplaceholder)
|
||||
.listener(object:RequestListener<File>{
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<File>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
Log.i("Glide","LoadFailed")
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: File?,
|
||||
model: Any?,
|
||||
target: Target<File>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val file = File(
|
||||
Environment.getExternalStorageDirectory(),
|
||||
DownloadHelper.defaultDir+".Images/" + imgUrl.substringAfterLast('/') + ".jpeg"
|
||||
) // the File to save , append increasing numeric counter to prevent files from getting overwritten.
|
||||
val options = BitmapFactory.Options()
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
val bitmap = BitmapFactory.decodeStream(FileInputStream(resource), null, options)
|
||||
resource?.copyTo(file)
|
||||
withContext(Dispatchers.Main){
|
||||
imgView.setImageBitmap(bitmap)
|
||||
// Log.i("Glide","imageSaved")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}).submit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*Extension Function For Copying Files!
|
||||
**/
|
||||
fun File.copyTo(file: File) {
|
||||
inputStream().use { input ->
|
||||
file.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun createDirectory(dir:String){
|
||||
val yourAppDir = File(Environment.getExternalStorageDirectory(),
|
||||
dir)
|
||||
|
||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||
{ // create empty directory
|
||||
if (yourAppDir.mkdirs())
|
||||
{Log.i("CreateDir","App dir created")}
|
||||
else
|
||||
{Log.w("CreateDir","Unable to create app dir!")}
|
||||
}
|
||||
else
|
||||
{Log.i("CreateDir","App dir already exists")}
|
||||
}
|
||||
|
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* 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.worker
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.arthenica.mobileffmpeg.Config
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
||||
import com.arthenica.mobileffmpeg.FFmpeg
|
||||
import com.mpatric.mp3agic.ID3v1Tag
|
||||
import com.mpatric.mp3agic.ID3v24Tag
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.spotiflyer.MainActivity
|
||||
import com.shabinder.spotiflyer.R
|
||||
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
|
||||
import com.shabinder.spotiflyer.models.DownloadObject
|
||||
import com.shabinder.spotiflyer.models.Track
|
||||
import com.tonyodev.fetch2.*
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
import com.tonyodev.fetch2core.Func
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
class ForegroundService : Service(){
|
||||
private val tag = "Foreground Service"
|
||||
private val channelId = "SpotiFlyer: Download Service"
|
||||
private var total = 0 //Total Downloads Requested
|
||||
private var converted = 0//Total Files Converted
|
||||
private var fetch:Fetch? = null
|
||||
private var downloadList = mutableListOf<DownloadObject>()
|
||||
private var serviceJob = Job()
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
|
||||
private val requestMap = mutableMapOf<Request,Track>()
|
||||
private val downloadMap = mutableMapOf<String,Track>()
|
||||
private var speed :Long = 0
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0, notificationIntent, 0
|
||||
)
|
||||
val notification = NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("SpotiFlyer: Downloading Your Music")
|
||||
.setSubText("Speed: $speed KB/s ")
|
||||
.setNotificationSilent()
|
||||
.setOnlyAlertOnce(true)
|
||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.build()
|
||||
|
||||
val fetchConfiguration =
|
||||
FetchConfiguration.Builder(this)
|
||||
.setDownloadConcurrentLimit(4)
|
||||
.build()
|
||||
|
||||
Fetch.Impl.setDefaultInstanceConfiguration(fetchConfiguration)
|
||||
fetch = Fetch.getDefaultInstance()
|
||||
fetch?.addListener(fetchListener)
|
||||
|
||||
startForeground(1, notification)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
// Send a notification that service is started
|
||||
Log.i(tag,"Service Started.")
|
||||
|
||||
//do heavy work on a background thread
|
||||
// val list = intent.getSerializableExtra("list") as List<Any?>
|
||||
val list = intent.getParcelableArrayListExtra<DownloadObject>("list") ?: intent.extras?.getParcelableArrayList<DownloadObject>("list")
|
||||
Log.i(tag,"Intent List Size: ${list!!.size}")
|
||||
total += list.size
|
||||
list.forEach { downloadList.add(it as DownloadObject) }
|
||||
|
||||
serviceScope.launch {
|
||||
withContext(Dispatchers.IO){
|
||||
for (downloadObject in downloadList) {
|
||||
val request= Request(downloadObject.url, downloadObject.outputDir)
|
||||
request.priority = Priority.NORMAL
|
||||
request.networkType = NetworkType.ALL
|
||||
|
||||
fetch?.enqueue(request,
|
||||
Func {
|
||||
Log.i("DownloadManager", "Download Request Sent")
|
||||
requestMap[it] = downloadObject.track
|
||||
downloadList.remove(downloadObject) },
|
||||
Func {
|
||||
Log.i("DownloadManager", "Download Request Error:${it.throwable.toString()}")}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if(downloadMap.isEmpty() && converted == total){
|
||||
Log.i(tag,"Service destroyed.")
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
if(downloadMap.isEmpty() && converted == total ){
|
||||
Log.i(tag,"Service destroyed.")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private var fetchListener: FetchListener = object : FetchListener {
|
||||
override fun onQueued(
|
||||
download: Download,
|
||||
waitingOnNetwork: Boolean
|
||||
) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onRemoved(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onResumed(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onStarted(
|
||||
download: Download,
|
||||
downloadBlocks: List<DownloadBlock>,
|
||||
totalBlocks: Int
|
||||
) {
|
||||
val track = requestMap[download.request]
|
||||
Log.i(tag,"${track?.name} Download Started")
|
||||
}
|
||||
|
||||
override fun onWaitingNetwork(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onAdded(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onCancelled(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onCompleted(download: Download) {
|
||||
val track = requestMap[download.request]
|
||||
speed = 0
|
||||
serviceScope.launch {
|
||||
convertToMp3(download.file, track!!)
|
||||
}
|
||||
Log.i(tag,"${track?.name} Download Completed")
|
||||
requestMap.remove(download.request)
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
override fun onDeleted(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onDownloadBlockUpdated(
|
||||
download: Download,
|
||||
downloadBlock: DownloadBlock,
|
||||
totalBlocks: Int
|
||||
) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||
Log.i(tag,download.error.throwable.toString())
|
||||
}
|
||||
|
||||
override fun onPaused(download: Download) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onProgress(
|
||||
download: Download,
|
||||
etaInMilliSeconds: Long,
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
val track = requestMap[download.request]
|
||||
Log.i(tag,"${track?.name} ETA: ${etaInMilliSeconds/1000} sec")
|
||||
speed = (downloadedBytesPerSecond/1000)
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun convertToMp3(filePath: String,track: Track){
|
||||
val m4aFile = File(filePath)
|
||||
|
||||
val executionId = FFmpeg.executeAsync(
|
||||
"-i $filePath -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
|
||||
) { _, returnCode ->
|
||||
when (returnCode) {
|
||||
RETURN_CODE_SUCCESS -> {
|
||||
Log.i(Config.TAG, "Async command execution completed successfully.")
|
||||
m4aFile.delete()
|
||||
writeMp3Tags(filePath.substringBeforeLast('.')+".mp3",track)
|
||||
//FFMPEG task Completed
|
||||
}
|
||||
RETURN_CODE_CANCEL -> {
|
||||
Log.i(Config.TAG, "Async command execution cancelled by user.")
|
||||
}
|
||||
else -> {
|
||||
Log.i(Config.TAG, String.format("Async command execution failed with rc=%d.", returnCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeMp3Tags(filePath:String, track: Track){
|
||||
var mp3File = Mp3File(filePath)
|
||||
mp3File = removeAllTags(mp3File)
|
||||
mp3File = setId3v1Tags(mp3File,track)
|
||||
mp3File = setId3v2Tags(mp3File,track)
|
||||
Log.i("Mp3Tags","saving file")
|
||||
mp3File.save(filePath.substringBeforeLast('.')+".new.mp3")
|
||||
val file = File(filePath)
|
||||
file.delete()
|
||||
val newFile = File((filePath.substringBeforeLast('.')+".new.mp3"))
|
||||
newFile.renameTo(file)
|
||||
converted++
|
||||
updateNotification()
|
||||
//All tasks completed (REST IN PEACE)
|
||||
if(converted == total){
|
||||
stopForeground(false)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* This is the method that can be called to update the Notification
|
||||
*/
|
||||
private fun updateNotification() {
|
||||
val mNotificationManager: NotificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notification = NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle("SpotiFlyer: Downloading Your Music")
|
||||
.setContentText("Total: $total Downloaded: ${total - requestMap.keys.size} Converted:$converted ")
|
||||
.setSubText("Speed: $speed KB/s ")
|
||||
.setNotificationSilent()
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(R.drawable.down_arrowbw)
|
||||
.build()
|
||||
mNotificationManager.notify(1, notification)
|
||||
}
|
||||
|
||||
private fun setId3v1Tags(mp3File: Mp3File, track: Track): Mp3File {
|
||||
val id3v1Tag = ID3v1Tag()
|
||||
id3v1Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v1Tag.artist = artistsList.joinToString()
|
||||
id3v1Tag.title = track.name
|
||||
id3v1Tag.album = track.album?.name
|
||||
id3v1Tag.year = track.album?.release_date
|
||||
id3v1Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
mp3File.id3v1Tag = id3v1Tag
|
||||
return mp3File
|
||||
}
|
||||
private fun setId3v2Tags(mp3file: Mp3File, track: Track): Mp3File {
|
||||
val id3v2Tag = ID3v24Tag()
|
||||
id3v2Tag.track = track.disc_number.toString()
|
||||
val artistsList = mutableListOf<String>()
|
||||
track.artists?.forEach { artistsList.add(it!!.name!!) }
|
||||
id3v2Tag.artist = artistsList.joinToString()
|
||||
id3v2Tag.title = track.name
|
||||
id3v2Tag.album = track.album?.name
|
||||
id3v2Tag.year = track.album?.release_date
|
||||
id3v2Tag.comment = "Genres:${track.album?.genres?.joinToString()}"
|
||||
id3v2Tag.lyrics = "Gonna Implement Soon"
|
||||
val copyrights = mutableListOf<String>()
|
||||
track.album?.copyrights?.forEach { copyrights.add(it!!.type!!) }
|
||||
id3v2Tag.copyright = copyrights.joinToString()
|
||||
id3v2Tag.url = track.href
|
||||
track.let {
|
||||
val file = File(
|
||||
Environment.getExternalStorageDirectory(),
|
||||
DownloadHelper.defaultDir +".Images/" + (it.album!!.images?.get(0)?.url!!).substringAfterLast('/') + ".jpeg")
|
||||
Log.i("Mp3Tags editing Tags",file.path)
|
||||
//init array with file length
|
||||
val bytesArray = ByteArray(file.length().toInt())
|
||||
val fis = FileInputStream(file)
|
||||
fis.read(bytesArray) //read file into bytes[]
|
||||
fis.close()
|
||||
id3v2Tag.setAlbumImage(bytesArray,"image/jpeg")
|
||||
}
|
||||
id3v2Tag.albumImage
|
||||
mp3file.id3v2Tag = id3v2Tag
|
||||
return mp3file
|
||||
}
|
||||
private fun removeAllTags(mp3file: Mp3File): Mp3File {
|
||||
if (mp3file.hasId3v1Tag()) {
|
||||
mp3file.removeId3v1Tag()
|
||||
}
|
||||
if (mp3file.hasId3v2Tag()) {
|
||||
mp3file.removeId3v2Tag()
|
||||
}
|
||||
if (mp3file.hasCustomTag()) {
|
||||
mp3file.removeCustomTag()
|
||||
}
|
||||
return mp3file
|
||||
}
|
||||
|
||||
}
|
BIN
app/src/main/res/drawable/down_arrowbw.png
Normal file
BIN
app/src/main/res/drawable/down_arrowbw.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
21
app/src/main/res/drawable/ic_github.xml
Normal file
21
app/src/main/res/drawable/ic_github.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<!--
|
||||
~ 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="38dp"
|
||||
android:height="38dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#5C6BC0" android:pathData="M255.968,5.329C114.624,5.329 0,120.401 0,262.353c0,113.536 73.344,209.856 175.104,243.872c12.8,2.368 17.472,-5.568 17.472,-12.384c0,-6.112 -0.224,-22.272 -0.352,-43.712c-71.2,15.52 -86.24,-34.464 -86.24,-34.464c-11.616,-29.696 -28.416,-37.6 -28.416,-37.6c-23.264,-15.936 1.728,-15.616 1.728,-15.616c25.696,1.824 39.2,26.496 39.2,26.496c22.848,39.264 59.936,27.936 74.528,21.344c2.304,-16.608 8.928,-27.936 16.256,-34.368c-56.832,-6.496 -116.608,-28.544 -116.608,-127.008c0,-28.064 9.984,-51.008 26.368,-68.992c-2.656,-6.496 -11.424,-32.64 2.496,-68c0,0 21.504,-6.912 70.4,26.336c20.416,-5.696 42.304,-8.544 64.096,-8.64c21.728,0.128 43.648,2.944 64.096,8.672c48.864,-33.248 70.336,-26.336 70.336,-26.336c13.952,35.392 5.184,61.504 2.56,68c16.416,17.984 26.304,40.928 26.304,68.992c0,98.72 -59.84,120.448 -116.864,126.816c9.184,7.936 17.376,23.616 17.376,47.584c0,34.368 -0.32,62.08 -0.32,70.496c0,6.88 4.608,14.88 17.6,12.352C438.72,472.145 512,375.857 512,262.353C512,120.401 397.376,5.329 255.968,5.329z"/>
|
||||
</vector>
|
25
app/src/main/res/drawable/ic_heart.xml
Normal file
25
app/src/main/res/drawable/ic_heart.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<!--
|
||||
~ 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="25dp"
|
||||
android:height="25dp" android:viewportWidth="512.007" android:viewportHeight="512.007">
|
||||
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fe646f" android:pathData="m380.125,59.036c-59.77,0 -109.664,42.249 -121.469,98.51 -0.608,2.899 -4.703,2.901 -5.312,0 -11.805,-56.261 -61.699,-98.51 -121.469,-98.51 -114.106,0 -167.756,141.01 -82.508,216.858l193.339,172.02c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#fd4755" android:pathData="m380.125,59.036c-6.912,0 -13.689,0.572 -20.293,1.658 99.376,15.991 141.363,144.168 61.527,215.2l-185.996,165.487 7.343,6.533c7.58,6.744 19.009,6.744 26.589,0l193.339,-172.02c85.248,-75.848 31.598,-216.858 -82.509,-216.858z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="m237.72,453.517c-204.315,-181.786 -197.402,-175.776 -197.402,-175.776 -25.999,-24.984 -40.318,-58.201 -40.318,-93.533 0,-46.48 24.63,-91.702 65.906,-115.47 3.589,-2.067 8.174,-0.833 10.242,2.757 2.067,3.589 0.833,8.175 -2.757,10.242 -36.017,20.74 -58.391,60.004 -58.391,102.471 0,31.212 12.683,60.588 35.711,82.717 0,0 -6.881,-5.996 196.979,175.386 2.292,2.039 5.242,3.161 8.309,3.161 3.066,0 6.018,-1.123 8.31,-3.162l61.917,-55.089c3.095,-2.753 7.835,-2.477 10.588,0.618s2.477,7.835 -0.618,10.588l-61.917,55.09c-10.431,9.281 -26.148,9.263 -36.559,0zM357.363,377.059c-2.067,0 -4.124,-0.849 -5.606,-2.515 -2.753,-3.095 -2.477,-7.835 0.618,-10.588l105.273,-93.665c21.815,-19.409 35.132,-44.369 38.513,-72.181 0.001,-0.006 0.001,-0.012 0.002,-0.018 7.637,-62.927 -37.915,-131.557 -116.038,-131.557 -54.879,0 -102.877,38.923 -114.129,92.55 -1.005,4.79 -5.116,8.135 -9.997,8.135s-8.991,-3.346 -9.996,-8.136c-11.252,-53.626 -59.25,-92.549 -114.128,-92.549 -9.633,0 -19.082,1.076 -28.084,3.198 -4.033,0.952 -8.07,-1.548 -9.021,-5.579 -0.951,-4.032 1.547,-8.07 5.579,-9.021 10.128,-2.388 20.735,-3.598 31.525,-3.598 55.699,0 105.463,35.109 124.125,87.792 18.71,-52.817 68.567,-87.792 124.125,-87.792 84.905,0 139.884,74.56 130.929,148.362 0,0.007 -0.001,0.015 -0.002,0.022 -3.829,31.494 -18.847,59.703 -43.433,81.578l-105.273,93.665c-1.429,1.272 -3.209,1.897 -4.982,1.897z"/>
|
||||
</vector>
|
51
app/src/main/res/drawable/ic_instagram.xml
Normal file
51
app/src/main/res/drawable/ic_instagram.xml
Normal file
@ -0,0 +1,51 @@
|
||||
<!--
|
||||
~ 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="32dp" android:height="32dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:pathData="M352,0H160C71.648,0 0,71.648 0,160v192c0,88.352 71.648,160 160,160h192c88.352,0 160,-71.648 160,-160V160C512,71.648 440.352,0 352,0zM464,352c0,61.76 -50.24,112 -112,112H160c-61.76,0 -112,-50.24 -112,-112V160C48,98.24 98.24,48 160,48h192c61.76,0 112,50.24 112,112V352z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="465.1312" android:endY="46.8656"
|
||||
android:startX="46.8688" android:startY="465.1344" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M256,128c-70.688,0 -128,57.312 -128,128s57.312,128 128,128s128,-57.312 128,-128S326.688,128 256,128zM256,336c-44.096,0 -80,-35.904 -80,-80c0,-44.128 35.904,-80 80,-80s80,35.872 80,80C336,300.096 300.096,336 256,336z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="346.5072" android:endY="165.4928"
|
||||
android:startX="165.4928" android:startY="346.5072" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M393.6,118.4m-17.056,0a17.056,17.056 0,1 1,34.112 0a17.056,17.056 0,1 1,-34.112 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:endX="405.6592" android:endY="106.3408"
|
||||
android:startX="381.5408" android:startY="130.4624" android:type="linear">
|
||||
<item android:color="#FFFFC107" android:offset="0"/>
|
||||
<item android:color="#FFF44336" android:offset="0.507"/>
|
||||
<item android:color="#FF9C27B0" android:offset="0.99"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
@ -1,5 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="42dp"
|
||||
android:height="42dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<!--
|
||||
~ 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="40dp"
|
||||
android:height="40dp" android:viewportWidth="512" android:viewportHeight="512">
|
||||
<path android:fillColor="#ff5d7d" android:fillType="evenOdd" android:pathData="m258.229,255.863c-11.191,-11.155 -29.503,-11.155 -40.693,0 -4.486,4.471 -11.053,4.007 -15.072,0 -11.191,-11.155 -29.503,-11.155 -40.693,0 -30.403,30.307 28.128,83.271 48.229,88.64 20.102,-5.369 78.632,-58.333 48.229,-88.64z"/>
|
||||
<path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="m258.229,255.863c30.403,30.307 -28.128,83.271 -48.23,88.64 -20.102,-5.369 -78.633,-58.334 -48.229,-88.64 11.191,-11.155 29.502,-11.155 40.693,0 4.02,4.007 10.587,4.471 15.072,0 11.191,-11.155 29.503,-11.155 40.694,0zM10,176c0,94.167 60,173.334 80,260h240c3.112,-13.487 7.193,-26.792 11.866,-40 4.742,-13.403 10.093,-26.707 15.66,-40 16.471,-39.33 34.83,-78.563 44.877,-119.994 3.154,-13.009 5.489,-26.235 6.689,-39.749 0.593,-6.679 0.908,-13.429 0.908,-20.257 0,-11 -9,-20 -20,-20 -120,0 -240,0 -360.001,0 -10.999,0 -19.999,9 -19.999,20z"/>
|
||||
<path android:fillColor="#ccf5fc" android:fillType="evenOdd" android:pathData="m402,356h-44.474c-5.567,13.293 -10.918,26.597 -15.66,40h60.134c55,0 99.999,-45 99.999,-100 0,-52.616 -41.185,-96.074 -92.908,-99.743 -1.2,13.514 -3.534,26.74 -6.69,39.749 32.818,0.218 59.599,27.129 59.599,59.994 0,33 -27,60 -60,60z"/>
|
||||
|
@ -134,7 +134,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraint_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
@ -240,6 +240,80 @@
|
||||
app:layout_constraintStart_toStartOf="@+id/btn_donate"
|
||||
app:layout_constraintTop_toBottomOf="@+id/usage" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/btn_github"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="130dp"
|
||||
android:background="@drawable/text_background"
|
||||
android:drawableStart="@drawable/ic_github"
|
||||
android:drawablePadding="5dp"
|
||||
android:fontFamily="@font/capriola"
|
||||
android:gravity="end|center_vertical"
|
||||
android:padding="5dp"
|
||||
android:text=" Github "
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_donate" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/developer_insta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="130dp"
|
||||
android:background="@drawable/text_background"
|
||||
android:drawableEnd="@drawable/ic_instagram"
|
||||
android:drawablePadding="5dp"
|
||||
android:fontFamily="@font/capriola"
|
||||
android:gravity="end|center_vertical"
|
||||
android:padding="5dp"
|
||||
android:text=" Follow Me "
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btn_donate" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tagLine"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:text="Made with "
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="22sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/heart"
|
||||
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/heart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="80dp"
|
||||
android:contentDescription="Made With Love In India"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/developer_insta"
|
||||
app:srcCompat="@drawable/ic_heart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tagLine2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginTop="80dp"
|
||||
android:fontFamily="@font/raleway_semibold"
|
||||
android:text=" in India"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="22sp"
|
||||
app:layout_constraintStart_toEndOf="@id/heart"
|
||||
app:layout_constraintTop_toBottomOf="@+id/developer_insta" />
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@ -252,11 +326,9 @@
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/open_spotify" />
|
||||
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
@ -1,5 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
~ Copyright (C) 2020 Shabinder Singh
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
@ -15,7 +32,6 @@
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="80dp"
|
||||
android:contentDescription="Track Image"
|
||||
android:visibility="visible"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/artist"
|
||||
|
25
app/src/main/res/xml/app_update.xml
Normal file
25
app/src/main/res/xml/app_update.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?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/>.
|
||||
-->
|
||||
|
||||
<AppUpdater>
|
||||
<update>
|
||||
<latestVersion>1.1</latestVersion>
|
||||
<latestVersionCode>2</latestVersionCode>
|
||||
<url>https://github.com/Shabinder/SpotiFlyer/releases</url>
|
||||
</update>
|
||||
</AppUpdater>
|
20
build.gradle
20
build.gradle
@ -1,3 +1,20 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext{
|
||||
@ -7,13 +24,14 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.0.1"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
//safe-Args
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user