Latest FFMPEG Async Tasks,Mp3 Art and Album Data,Fetch Downloader Implemented.

This commit is contained in:
shabinder 2020-07-30 12:53:56 +05:30
parent ac683279e6
commit a802751942
38 changed files with 1201 additions and 205 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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'

View File

@ -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.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>

View 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)
}
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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 {
val format: Format =
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
val audioUrl = format.url()
Log.i("DHelper Link Found", audioUrl)
if (audioUrl != null) {
downloadFile(audioUrl, downloadManager, details!!.title(),subFolder,type)
val audioUrl = getDownloadLink(AudioQuality.medium, ytDownloader, data)
withContext(Dispatchers.Main) {
mainFragment?.showToast("Download Started")
mainFragment?.showToast("Starting Download")
}
} else {
Log.i("YT audio url is null", format.toString())
}
}catch (e:ArrayIndexOutOfBoundsException){
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
} catch (e: java.lang.IndexOutOfBoundsException) {
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)
val audioUrl = getDownloadLink(AudioQuality.high, ytDownloader, data)
withContext(Dispatchers.Main) {
mainFragment?.showToast("Download Started")
mainFragment?.showToast("Starting Download")
}
} else {
Log.i("YT audio url is null", format.toString())
}
}catch (e:ArrayIndexOutOfBoundsException){
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
} catch (e: java.lang.IndexOutOfBoundsException) {
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)
val audioUrl = getDownloadLink(AudioQuality.low, ytDownloader, data)
withContext(Dispatchers.Main) {
mainFragment?.showToast("Download Started")
mainFragment?.showToast("Starting Download")
}
} else {
Log.i("YT audio url is null", format.toString())
}
}catch(e:ArrayIndexOutOfBoundsException){
downloadFile(audioUrl, searchQuery, subFolder, type, track, index,mainFragment)
} catch (e: java.lang.IndexOutOfBoundsException) {
Log.i("Catch", e.toString())
}
}
}
}
}
}
/**
* Downloading Using Android Download Manager
* */
suspend fun downloadFile(url: String, downloadManager: DownloadManager?, title: String,subFolder: String?,type: String) {
private fun getDownloadLink(quality: AudioQuality ,ytDownloader: YoutubeDownloader?,data:YoutubeInterface.VideoItem): String {
val video = ytDownloader?.getVideo(data.id)
val format: Format =
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)
return audioUrl
}
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
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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")}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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>

View 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>

View 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>

View File

@ -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"/>

View File

@ -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>

View File

@ -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"

View 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>

View File

@ -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
}