changelog same as 1.7 release

This commit is contained in:
Shabinder 2020-12-02 21:55:14 +05:30
parent 2e40db5452
commit b96008a553
40 changed files with 321 additions and 599 deletions

View File

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

View File

@ -15,6 +15,7 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@ -8,7 +8,7 @@
<component name="ProjectPlainTextFileTypeManager"> <component name="ProjectPlainTextFileTypeManager">
<file url="file://$PROJECT_DIR$/app/src/main/java/com/shabinder/spotiflyer/testing/YoutubeInterface.kt.backup" /> <file url="file://$PROJECT_DIR$/app/src/main/java/com/shabinder/spotiflyer/testing/YoutubeInterface.kt.backup" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@ -19,7 +19,7 @@ plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'kotlin-android-extensions' id 'kotlin-parcelize'
id 'androidx.navigation.safeargs.kotlin' id 'androidx.navigation.safeargs.kotlin'
id 'dagger.hilt.android.plugin' id 'dagger.hilt.android.plugin'
id 'kotlinx-serialization' id 'kotlinx-serialization'
@ -39,8 +39,8 @@ android {
applicationId 'com.shabinder.spotiflyer' applicationId 'com.shabinder.spotiflyer'
minSdkVersion 22 minSdkVersion 22
targetSdkVersion 30 targetSdkVersion 30
versionCode 8 versionCode 9
versionName "1.6" versionName "1.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -79,6 +79,7 @@ android {
exclude 'META-INF/ASL2.0' exclude 'META-INF/ASL2.0'
exclude("META-INF/*.kotlin_module") exclude("META-INF/*.kotlin_module")
} }
ndkVersion '21.3.6528147'
} }
@ -101,7 +102,7 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1' implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.2.1' implementation 'com.google.android.material:material:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2-native-mt'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"

View File

@ -37,7 +37,6 @@ import androidx.navigation.findNavController
import com.github.javiersantos.appupdater.AppUpdater import com.github.javiersantos.appupdater.AppUpdater
import com.github.javiersantos.appupdater.enums.UpdateFrom import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.shabinder.spotiflyer.databinding.MainActivityBinding import com.shabinder.spotiflyer.databinding.MainActivityBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest import com.shabinder.spotiflyer.networking.SpotifyServiceTokenRequest
import com.shabinder.spotiflyer.utils.NetworkInterceptor import com.shabinder.spotiflyer.utils.NetworkInterceptor
@ -45,6 +44,7 @@ import com.shabinder.spotiflyer.utils.createDirectories
import com.shabinder.spotiflyer.utils.showMessage import com.shabinder.spotiflyer.utils.showMessage
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -59,6 +59,8 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(){ class MainActivity : AppCompatActivity(){
private var spotifyService : SpotifyService? = null private var spotifyService : SpotifyService? = null
val viewModelScope : CoroutineScope
get() = sharedViewModel.viewModelScope
private lateinit var binding: MainActivityBinding private lateinit var binding: MainActivityBinding
private lateinit var sharedViewModel: SharedViewModel private lateinit var sharedViewModel: SharedViewModel
lateinit var snackBarAnchor: View lateinit var snackBarAnchor: View
@ -75,7 +77,6 @@ class MainActivity : AppCompatActivity(){
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java) sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
navController = findNavController(R.id.navHostFragment) navController = findNavController(R.id.navHostFragment)
snackBarAnchor = binding.snackBarPosition snackBarAnchor = binding.snackBarPosition
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
authenticateSpotify() authenticateSpotify()
} }

View File

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

View File

@ -1,168 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.downloadHelper
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.widget.TextView
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import com.tonyodev.fetch2.Status
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File
object DownloadHelper {
var statusBar:TextView? = null
var youtubeMusicApi: YoutubeMusicApi? = null
private var total = 0
private var processed = 0
var notFound = 0
/**
* Function To Download All Tracks Available in a List
**/
suspend fun downloadAllTracks(
type:String,
subFolder: String?,
trackList: List<TrackDetails>) {
resetStatusBar()// For New Download Request's Status
val downloadList = ArrayList<DownloadObject>()
withContext(Dispatchers.IO){
total += trackList.size // Adding New Download List Count to StatusBar
trackList.forEachIndexed { index, it ->
if(!isOnline()){
showNoConnectionAlert()
return@withContext
}
if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
processed++
if(index == (trackList.size-1)){//LastElement
Handler(Looper.myLooper()!!).postDelayed({
//Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper","Download Request Sent")
showMessage("Download Started, Now You can leave the App!")
startService(mainActivity,downloadList)
},3000)
}
}else{
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
youtubeMusicApi?.getYoutubeMusicResponse(jsonBody)?.enqueue(
object : Callback<String>{
override fun onResponse(call: Call<String>, response: Response<String>) {
val videoId = sortByBestMatch(
getYTTracks(response.body().toString()),
trackName = it.title,
trackArtists = it.artists,
trackDurationSec = it.durationSec
).keys.firstOrNull()
Log.i("Spotify Helper Video ID",videoId ?: "Not Found")
if(videoId.isNullOrBlank()) {
//Track Not Found
notFound++ ; updateStatusBar()
val intent = Intent()
.setAction(Status.FAILED.name)
.putExtra("track",it)
statusBar?.context?.sendBroadcast(intent)
}
else {//Found Youtube Video ID
val outputFile: String =
defaultDir +
removeIllegalChars(type) + File.separator +
(if (subFolder == null) { "" }
else { removeIllegalChars(subFolder) + File.separator }
+ removeIllegalChars(it.title) + ".m4a")
val downloadObject = DownloadObject(
trackDetails = it,
ytVideoId = videoId,
outputFile = outputFile
)
processed++
updateStatusBar()
downloadList.add(downloadObject)
}
if(index == (trackList.size-1)){//LastElement
statusBar?.clearAnimation()
if(downloadList.size > 0) {
Handler(Looper.myLooper()!!).postDelayed({
//Delay is Added ,if a request is in processing it may finish
Log.i("Spotify Helper", "Download Request Sent")
showMessage("Download Started, Now You can leave the App!")
startService(mainActivity, downloadList)
}, 3000)
}
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
if(t.message.toString().contains("Failed to connect")) showMessage("Failed, Check Your Internet Connection!")
Log.i("YT API Req. Fail",t.message.toString())
}
}
)
}
updateStatusBar()
}
animateStatusBar()
}
}
private fun resetStatusBar() {
total = 0
processed = 0
notFound = 0
updateStatusBar()
}
private fun animateStatusBar() {
val anim: Animation = AlphaAnimation(0.3f, 0.9f)
anim.duration = 1500 //You can manage the blinking time with this parameter
anim.startOffset = 20
anim.repeatMode = Animation.REVERSE
anim.repeatCount = Animation.INFINITE
statusBar?.animation = anim
}
@SuppressLint("SetTextI18n")
private fun updateStatusBar() {
CoroutineScope(Dispatchers.Main).launch{
statusBar!!.visibility = View.VISIBLE
statusBar?.text = "Total: $total ${getEmojiByUnicode(0x2705)}: $processed ${getEmojiByUnicode(0x274C)}: $notFound"
}
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright (C) 2020 Shabinder Singh
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.spotiflyer.downloadHelper
import android.util.Log
import com.shabinder.spotiflyer.models.DownloadObject
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.mainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
interface YTDownloadHelper {
suspend fun downloadYTTracks(
type:String,
subFolder: String?,
tracks:List<TrackDetails>,
){
val downloadList = ArrayList<DownloadObject>()
tracks.forEach {
if(!isOnline()){
showNoConnectionAlert()
return
}
val outputFile: String = defaultDir +
removeIllegalChars(type) + File.separator +
(if (subFolder == null) { "" }
else { removeIllegalChars(subFolder) + File.separator }
+ removeIllegalChars(it.title) + ".m4a")
val downloadObject = DownloadObject(
trackDetails = it,
ytVideoId = it.albumArt.absolutePath.substringAfterLast("/")
.substringBeforeLast("."),
outputFile = outputFile
)
downloadList.add(downloadObject)
}
Log.i("YT Downloader Helper","Download Request Sent")
withContext(Dispatchers.Main){
showMessage("Download Started, Now You can leave the App!")
startService(mainActivity,downloadList)
}
}
}

View File

@ -19,16 +19,9 @@ package com.shabinder.spotiflyer.models
import android.os.Parcelable import android.os.Parcelable
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
@Parcelize
data class DownloadObject(
var trackDetails: TrackDetails,
var ytVideoId:String,
var outputFile:String
):Parcelable
@Parcelize @Parcelize
data class TrackDetails( data class TrackDetails(
var title:String, var title:String,
@ -43,7 +36,9 @@ data class TrackDetails(
var albumArtURL: String, var albumArtURL: String,
var source: Source, var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded, var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var progress: Int = 0 var progress: Int = 0,
var outputFile: String,
var videoID:String? = null
):Parcelable ):Parcelable
enum class DownloadStatus{ enum class DownloadStatus{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,8 +27,6 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListItemBinding import com.shabinder.spotiflyer.databinding.TrackListItemBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
@ -36,7 +34,7 @@ import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListViewModel
import com.shabinder.spotiflyer.utils.* import com.shabinder.spotiflyer.utils.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()),YTDownloadHelper { class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<TrackDetails, TrackListAdapter.ViewHolder>(TrackDiffCallback()){
var source:Source =Source.Spotify var source:Source =Source.Spotify
@ -115,20 +113,12 @@ class TrackListAdapter(private val viewModel : TrackListViewModel): ListAdapter<
when(source){ when(source){
Source.YouTube -> { Source.YouTube -> {
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
downloadYTTracks( downloadTracks(arrayListOf(item))
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
} }
} }
else -> { else -> {
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
DownloadHelper.downloadAllTracks( downloadTracks(arrayListOf(item))
viewModel.folderType,
viewModel.subFolder,
listOf(item)
)
} }
} }
} }

View File

@ -32,5 +32,6 @@ abstract class BaseFragment<VB:ViewBinding,VM : ViewModel> : Fragment() {
protected abstract val viewModel: VM protected abstract val viewModel: VM
protected val viewModelScope by lazy{viewModel.viewModelScope} protected val viewModelScope by lazy{viewModel.viewModelScope}
open fun applicationContext(): Context = requireActivity().applicationContext protected val applicationContext: Context
get() = requireActivity().applicationContext
} }

View File

@ -29,7 +29,6 @@ import android.view.ViewGroup
import androidx.navigation.NavArgs import androidx.navigation.NavArgs
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding import com.shabinder.spotiflyer.databinding.TrackListFragmentBinding
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
@ -63,15 +62,9 @@ abstract class TrackListFragment<VM : TrackListViewModel, args: NavArgs> : BaseF
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
binding = TrackListFragmentBinding.inflate(inflater,container,false) binding = TrackListFragmentBinding.inflate(inflater,container,false)
initializeAll()
return binding.root return binding.root
} }
private fun initializeAll() {
DownloadHelper.youtubeMusicApi = sharedViewModel.youtubeMusicApi
DownloadHelper.statusBar = binding.statusBar
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.trackList.adapter = adapter binding.trackList.adapter = adapter
@ -84,7 +77,7 @@ abstract class TrackListFragment<VM : TrackListViewModel, args: NavArgs> : BaseF
private fun initializeLiveDataObservers() { private fun initializeLiveDataObservers() {
viewModel.trackList.observe(viewLifecycleOwner, { viewModel.trackList.observe(viewLifecycleOwner, {
if (!it.isNullOrEmpty()){ if (!it.isNullOrEmpty()){
Log.i("GaanaFragment","TrackList Updated") Log.i("TrackListFragment","TrackList Updated")
adapter.submitList(it, source) adapter.submitList(it, source)
updateTracksStatus() updateTracksStatus()
} }

View File

@ -25,8 +25,8 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
@ -46,7 +46,7 @@ class GaanaFragment : TrackListFragment<GaanaViewModel, GaanaFragmentArgs>() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
adapter = TrackListAdapter(viewModel) adapter = TrackListAdapter(viewModel)
@ -76,24 +76,21 @@ class GaanaFragment : TrackListFragment<GaanaViewModel, GaanaFragmentArgs>() {
visible() visible()
rotate() rotate()
} }
for (track in viewModel.trackList.value!!){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.viewModelScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana) loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.Gaana)
} }
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value?.filter{it.downloaded == DownloadStatus.NotDownloaded}
if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song") if(finalList.isNullOrEmpty())showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( finalList?.let { it1 -> downloadTracks(it1 as ArrayList<TrackDetails>) }
viewModel.folderType, for (track in viewModel.trackList.value!!){
viewModel.subFolder, if(track.downloaded == DownloadStatus.NotDownloaded){
finalList ?: listOf(), track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
} }
} }
} }

View File

@ -37,7 +37,7 @@ import java.io.File
class GaanaViewModel @ViewModelInject constructor( class GaanaViewModel @ViewModelInject constructor(
val databaseDAO: DatabaseDAO, val databaseDAO: DatabaseDAO,
val gaanaInterface : GaanaInterface private val gaanaInterface : GaanaInterface
) : TrackListViewModel(){ ) : TrackListViewModel(){
override var folderType:String = "" override var folderType:String = ""
@ -51,6 +51,7 @@ class GaanaViewModel @ViewModelInject constructor(
"song" -> { "song" -> {
gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also { gaanaInterface.getGaanaSong(seokey = link).value?.tracks?.firstOrNull()?.also {
folderType = "Tracks" folderType = "Tracks"
subFolder = ""
if (File( if (File(
finalOutputDir( finalOutputDir(
it.track_title, it.track_title,
@ -61,7 +62,7 @@ class GaanaViewModel @ViewModelInject constructor(
) {//Download Already Present!! ) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
} }
trackList.value = listOf(it).toTrackDetailsList() trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.track_title title.value = it.track_title
coverUrl.value = it.artworkLink coverUrl.value = it.artworkLink
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -72,12 +73,6 @@ class GaanaViewModel @ViewModelInject constructor(
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value!!,
totalFiles = 1, totalFiles = 1,
downloaded = it.downloaded == DownloadStatus.Downloaded,
directory = finalOutputDir(
it.track_title,
folderType,
subFolder
)
) )
) )
} }
@ -99,7 +94,7 @@ class GaanaViewModel @ViewModelInject constructor(
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link title.value = link
coverUrl.value = it.custom_artworks.size_480p coverUrl.value = it.custom_artworks.size_480p
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -110,16 +105,6 @@ class GaanaViewModel @ViewModelInject constructor(
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(
finalOutputDir(
type = folderType,
subFolder = subFolder
)
).listFiles()?.size == trackList.value?.size,
directory = finalOutputDir(
type = folderType,
subFolder = subFolder
)
) )
) )
} }
@ -141,7 +126,7 @@ class GaanaViewModel @ViewModelInject constructor(
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
title.value = link title.value = link
//coverUrl.value = "TODO" //coverUrl.value = "TODO"
coverUrl.value = gaanaPlaceholderImageUrl coverUrl.value = gaanaPlaceholderImageUrl
@ -153,16 +138,6 @@ class GaanaViewModel @ViewModelInject constructor(
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = it.tracks.size, totalFiles = it.tracks.size,
downloaded = File(
finalOutputDir(
type = folderType,
subFolder = subFolder
)
).listFiles()?.size == trackList.value?.size,
directory = finalOutputDir(
type = folderType,
subFolder = subFolder
)
) )
) )
} }
@ -190,7 +165,7 @@ class GaanaViewModel @ViewModelInject constructor(
track.downloaded = DownloadStatus.Downloaded track.downloaded = DownloadStatus.Downloaded
} }
} }
trackList.value = it.tracks.toTrackDetailsList() trackList.value = it.tracks.toTrackDetailsList(folderType, subFolder)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
databaseDAO.insert( databaseDAO.insert(
DownloadRecord( DownloadRecord(
@ -199,16 +174,6 @@ class GaanaViewModel @ViewModelInject constructor(
link = "https://gaana.com/$type/$link", link = "https://gaana.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(
finalOutputDir(
type = folderType,
subFolder = subFolder
)
).listFiles()?.size == trackList.value?.size,
directory = finalOutputDir(
type = folderType,
subFolder = subFolder
)
) )
) )
} }
@ -219,7 +184,7 @@ class GaanaViewModel @ViewModelInject constructor(
} }
} }
private fun List<GaanaTrack>.toTrackDetailsList() = this.map { private fun List<GaanaTrack>.toTrackDetailsList(type:String , subFolder:String) = this.map {
TrackDetails( TrackDetails(
title = it.track_title, title = it.track_title,
artists = it.artist.map { artist -> artist?.name.toString() }, artists = it.artist.map { artist -> artist?.name.toString() },
@ -232,7 +197,8 @@ class GaanaViewModel @ViewModelInject constructor(
trackUrl = it.lyrics_url, trackUrl = it.lyrics_url,
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana, source = Source.Gaana,
albumArtURL = it.artworkLink albumArtURL = it.artworkLink,
outputFile = finalOutputDir(it.track_title,type, subFolder,".m4a")
) )
}.toMutableList() }.toMutableList()
} }

View File

@ -51,7 +51,7 @@ class MainFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
binding = MainFragmentBinding.inflate(inflater,container,false) binding = MainFragmentBinding.inflate(inflater,container,false)
initializeAll() initializeAll()
binding.btnSearch.setOnClickListener { binding.btnSearch.setOnClickListener {
@ -86,12 +86,6 @@ class MainFragment : Fragment() {
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//starting Notification and Downloader Service!
startService(requireContext())
}
/** /**
* Handle Intent If there is any! * Handle Intent If there is any!
**/ **/

View File

@ -26,8 +26,8 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.DownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.SpotifyService import com.shabinder.spotiflyer.networking.SpotifyService
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
@ -103,16 +103,6 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel, SpotifyFragmentArgs>
visible() visible()
rotate() rotate()
} }
for (track in viewModel.trackList.value ?: listOf()) {
if (track.downloaded != DownloadStatus.Downloaded) {
track.downloaded = DownloadStatus.Queued
adapter.notifyItemChanged(
viewModel.trackList.value!!.indexOf(
track
)
)
}
}
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.viewModelScope.launch(Dispatchers.Default) { sharedViewModel.viewModelScope.launch(Dispatchers.Default) {
loadAllImages( loadAllImages(
@ -122,13 +112,16 @@ class SpotifyFragment : TrackListFragment<SpotifyViewModel, SpotifyFragmentArgs>
) )
} }
viewModelScope.launch { viewModelScope.launch {
val finalList = viewModel.trackList.value val finalList = viewModel.trackList.value?.filter{it.downloaded == DownloadStatus.NotDownloaded}
if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song") if (finalList.isNullOrEmpty()) showMessage("Not Downloading Any Song")
DownloadHelper.downloadAllTracks( else downloadTracks(finalList as ArrayList<TrackDetails>)
viewModel.folderType, for (track in viewModel.trackList.value ?: listOf()) {
viewModel.subFolder, if (track.downloaded == DownloadStatus.NotDownloaded) {
finalList ?: listOf(), track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
} }
} }
} }

View File

@ -61,9 +61,10 @@ class SpotifyViewModel @ViewModelInject constructor(
"track" -> { "track" -> {
spotifyService?.getTrack(link)?.value?.also { spotifyService?.getTrack(link)?.value?.also {
folderType = "Tracks" folderType = "Tracks"
subFolder = ""
if (File( if (File(
finalOutputDir( finalOutputDir(
it.name, it.name.toString(),
folderType, folderType,
subFolder subFolder
) )
@ -71,7 +72,7 @@ class SpotifyViewModel @ViewModelInject constructor(
) {//Download Already Present!! ) {//Download Already Present!!
it.downloaded = DownloadStatus.Downloaded it.downloaded = DownloadStatus.Downloaded
} }
trackList.value = listOf(it).toTrackDetailsList() trackList.value = listOf(it).toTrackDetailsList(folderType, subFolder)
title.value = it.name title.value = it.name
coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url coverUrl.value = it.album!!.images?.elementAtOrNull(1)?.url
?: it.album!!.images?.elementAtOrNull(0)?.url ?: it.album!!.images?.elementAtOrNull(0)?.url
@ -83,8 +84,6 @@ class SpotifyViewModel @ViewModelInject constructor(
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value!!, coverUrl = coverUrl.value!!,
totalFiles = 1, totalFiles = 1,
downloaded = it.downloaded == DownloadStatus.Downloaded,
directory = finalOutputDir(it.name, folderType, subFolder)
) )
) )
} }
@ -115,7 +114,7 @@ class SpotifyViewModel @ViewModelInject constructor(
) )
) )
} }
trackList.value = albumObject?.tracks?.items?.toTrackDetailsList() trackList.value = albumObject?.tracks?.items?.toTrackDetailsList(folderType, subFolder)
title.value = albumObject?.name title.value = albumObject?.name
coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url coverUrl.value = albumObject?.images?.elementAtOrNull(1)?.url
?: albumObject?.images?.elementAtOrNull(0)?.url ?: albumObject?.images?.elementAtOrNull(0)?.url
@ -127,13 +126,6 @@ class SpotifyViewModel @ViewModelInject constructor(
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = trackList.value?.size ?: 0, totalFiles = trackList.value?.size ?: 0,
downloaded = File(
finalOutputDir(
type = folderType,
subFolder = subFolder
)
).listFiles()?.size == trackList.value?.size,
directory = finalOutputDir(type = folderType, subFolder = subFolder)
) )
) )
} }
@ -172,7 +164,7 @@ class SpotifyViewModel @ViewModelInject constructor(
moreTracksAvailable = !moreTracks?.next.isNullOrBlank() moreTracksAvailable = !moreTracks?.next.isNullOrBlank()
} }
Log.i("Total Tracks Fetched", tempTrackList.size.toString()) Log.i("Total Tracks Fetched", tempTrackList.size.toString())
trackList.value = tempTrackList.toTrackDetailsList() trackList.value = tempTrackList.toTrackDetailsList(folderType, subFolder)
title.value = playlistObject?.name title.value = playlistObject?.name
coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url coverUrl.value = playlistObject?.images?.elementAtOrNull(1)?.url
?: playlistObject?.images?.firstOrNull()?.url.toString() ?: playlistObject?.images?.firstOrNull()?.url.toString()
@ -184,13 +176,6 @@ class SpotifyViewModel @ViewModelInject constructor(
link = "https://open.spotify.com/$type/$link", link = "https://open.spotify.com/$type/$link",
coverUrl = coverUrl.value.toString(), coverUrl = coverUrl.value.toString(),
totalFiles = tempTrackList.size, totalFiles = tempTrackList.size,
downloaded = File(
finalOutputDir(
type = folderType,
subFolder = subFolder
)
).listFiles()?.size == tempTrackList.size,
directory = finalOutputDir(type = folderType, subFolder = subFolder)
) )
) )
} }
@ -204,8 +189,7 @@ class SpotifyViewModel @ViewModelInject constructor(
} }
} }
@Suppress("DEPRECATION") private fun List<Track>.toTrackDetailsList(type:String , subFolder:String) = this.map {
private fun List<Track>.toTrackDetailsList() = this.map {
TrackDetails( TrackDetails(
title = it.name.toString(), title = it.name.toString(),
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
@ -218,7 +202,8 @@ class SpotifyViewModel @ViewModelInject constructor(
trackUrl = it.href, trackUrl = it.href,
downloaded = it.downloaded, downloaded = it.downloaded,
source = Source.Spotify, source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString() albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFile = finalOutputDir(it.name.toString(),type, subFolder,".m4a")
) )
}.toMutableList() }.toMutableList()
} }

View File

@ -24,8 +24,8 @@ import android.view.ViewGroup
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.shabinder.spotiflyer.downloadHelper.YTDownloadHelper
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.recyclerView.TrackListAdapter import com.shabinder.spotiflyer.recyclerView.TrackListAdapter
import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment import com.shabinder.spotiflyer.ui.base.tracklistbase.TrackListFragment
@ -38,7 +38,7 @@ private const val sampleDomain2 = "youtu.be"
private const val sampleDomain1 = "youtube.com" private const val sampleDomain1 = "youtube.com"
@AndroidEntryPoint @AndroidEntryPoint
class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>() , YTDownloadHelper { class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>(){
override val viewModel: YoutubeViewModel by viewModels() override val viewModel: YoutubeViewModel by viewModels()
override lateinit var adapter : TrackListAdapter override lateinit var adapter : TrackListAdapter
@ -48,7 +48,7 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
adapter = TrackListAdapter(viewModel) adapter = TrackListAdapter(viewModel)
@ -90,24 +90,19 @@ class YoutubeFragment : TrackListFragment<YoutubeViewModel, YoutubeFragmentArgs>
visible() visible()
rotate() rotate()
} }
for (track in this.viewModel.trackList.value?: listOf()){
if(track.downloaded != DownloadStatus.Downloaded){
track.downloaded = DownloadStatus.Queued
//adapter.notifyItemChanged(this.viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
showMessage("Processing!") showMessage("Processing!")
sharedViewModel.viewModelScope.launch(Dispatchers.Default){ sharedViewModel.viewModelScope.launch(Dispatchers.Default){
loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube) loadAllImages(requireActivity(), viewModel.trackList.value?.map{it.albumArtURL}, Source.YouTube)
} }
viewModel.viewModelScope.launch { viewModel.viewModelScope.launch {
downloadYTTracks( downloadTracks((viewModel.trackList.value?.filter { it.downloaded == DownloadStatus.NotDownloaded } ?: arrayListOf()) as ArrayList<TrackDetails>)
type = viewModel.folderType, for (track in viewModel.trackList.value?: listOf()){
subFolder = viewModel.subFolder, if(track.downloaded == DownloadStatus.NotDownloaded){
tracks = viewModel.trackList.value ?: listOf() track.downloaded = DownloadStatus.Queued
) //adapter.notifyItemChanged(viewModel.trackList.value!!.indexOf(track))
}
}
adapter.notifyDataSetChanged()
} }
} }
} }

View File

@ -82,7 +82,9 @@ class YoutubeViewModel @ViewModelInject constructor(
DownloadStatus.Downloaded DownloadStatus.Downloaded
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
} },
outputFile = finalOutputDir(it.title(),folderType, subFolder,".m4a"),
videoID = it.videoId()
) )
}.toMutableList()) }.toMutableList())
@ -93,8 +95,6 @@ class YoutubeViewModel @ViewModelInject constructor(
link = "https://www.youtube.com/playlist?list=$searchId", link = "https://www.youtube.com/playlist?list=$searchId",
coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg", coverUrl = "https://i.ytimg.com/vi/${videos.firstOrNull()?.videoId()}/hqdefault.jpg",
totalFiles = videos.size, totalFiles = videos.size,
directory = finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder),
downloaded = File(finalOutputDir(itemName = removeIllegalChars(name),type = folderType,subFolder = subFolder)).exists()
)) ))
} }
queryActiveTracks() queryActiveTracks()
@ -124,8 +124,21 @@ class YoutubeViewModel @ViewModelInject constructor(
durationSec = detail?.lengthSeconds()?:0, durationSec = detail?.lengthSeconds()?:0,
albumArt = File(imageDir,"$searchId.jpeg"), albumArt = File(imageDir,"$searchId.jpeg"),
source = Source.YouTube, source = Source.YouTube,
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg" albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
) downloaded = if (File(
finalOutputDir(
itemName = name,
type = folderType,
subFolder = subFolder
)).exists()
)
DownloadStatus.Downloaded
else {
DownloadStatus.NotDownloaded
},
outputFile = finalOutputDir(name,folderType, subFolder,".m4a"),
videoID = searchId
)
).toMutableList() ).toMutableList()
) )
title.postValue( title.postValue(
@ -139,8 +152,6 @@ class YoutubeViewModel @ViewModelInject constructor(
link = "https://www.youtube.com/watch?v=$searchId", link = "https://www.youtube.com/watch?v=$searchId",
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
totalFiles = 1, totalFiles = 1,
downloaded = false,
directory = finalOutputDir(type = "YT_Downloads")
)) ))
} }
queryActiveTracks() queryActiveTracks()

View File

@ -34,7 +34,7 @@ import com.bumptech.glide.request.target.Target
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider.defaultDir import com.shabinder.spotiflyer.utils.Provider.defaultDir
import com.shabinder.spotiflyer.utils.Provider.imageDir import com.shabinder.spotiflyer.utils.Provider.imageDir
@ -52,11 +52,17 @@ fun loadAllImages(context: Context?, images:List<String>? = null,source:Source)
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun startService(context:Context? = mainActivity,objects:ArrayList<DownloadObject>? = null ) { fun downloadTracks(
val serviceIntent = Intent(context, ForegroundService::class.java) trackList: ArrayList<TrackDetails>,
objects?.let { serviceIntent.putParcelableArrayListExtra("object",it) } context: Context? = mainActivity
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } ) {
if(!trackList.isNullOrEmpty()){
val serviceIntent = Intent(context, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("object",trackList)
context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
} }
fun queryActiveTracks(context:Context? = mainActivity) { fun queryActiveTracks(context:Context? = mainActivity) {
val serviceIntent = Intent(context, ForegroundService::class.java).apply { val serviceIntent = Intent(context, ForegroundService::class.java).apply {
action = "query" action = "query"
@ -64,10 +70,10 @@ fun queryActiveTracks(context:Context? = mainActivity) {
context?.let { ContextCompat.startForegroundService(it, serviceIntent) } context?.let { ContextCompat.startForegroundService(it, serviceIntent) }
} }
fun finalOutputDir(itemName:String? = null,type:String, subFolder:String?=null,extension:String? = ".mp3"): String{ fun finalOutputDir(itemName:String ,type:String, subFolder:String,extension:String = ".mp3"): String{
return defaultDir + removeIllegalChars(type) + File.separator + return defaultDir + removeIllegalChars(type) + File.separator +
(if(subFolder == null){""}else{ removeIllegalChars(subFolder) + File.separator} if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + File.separator} +
+ itemName?.let { removeIllegalChars(it) + extension}) removeIllegalChars(itemName) + extension
} }
/** /**
@ -220,7 +226,7 @@ fun createDirectory(dir:String){
/** /**
* Removing Illegal Chars from File Name * Removing Illegal Chars from File Name
* **/ * **/
fun removeIllegalChars(fileName: String): String? { fun removeIllegalChars(fileName: String): String {
val illegalCharArray = charArrayOf( val illegalCharArray = charArrayOf(
'/', '/',
'\n', '\n',
@ -266,6 +272,3 @@ fun createDirectories() {
createDirectory(defaultDir + "Playlists/") createDirectory(defaultDir + "Playlists/")
createDirectory(defaultDir + "YT_Downloads/") createDirectory(defaultDir + "YT_Downloads/")
} }
fun getEmojiByUnicode(unicode: Int): String? {
return String(Character.toChars(unicode))
}

View File

@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.util.Log import android.util.Log
@ -46,21 +47,29 @@ import com.mpatric.mp3agic.ID3v1Tag
import com.mpatric.mp3agic.ID3v24Tag import com.mpatric.mp3agic.ID3v24Tag
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.spotiflyer.R import com.shabinder.spotiflyer.R
import com.shabinder.spotiflyer.models.DownloadObject import com.shabinder.spotiflyer.downloadHelper.getYTTracks
import com.shabinder.spotiflyer.downloadHelper.sortByBestMatch
import com.shabinder.spotiflyer.models.DownloadStatus import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.utils.Provider import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.utils.*
import com.shabinder.spotiflyer.utils.Provider.imageDir import com.shabinder.spotiflyer.utils.Provider.imageDir
import com.shabinder.spotiflyer.utils.copyTo
import com.tonyodev.fetch2.* import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.* import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
@RequiresApi(Build.VERSION_CODES.O)
class ForegroundService : Service(){ class ForegroundService : Service(){
private val tag = "Foreground Service" private val tag = "Foreground Service"
private val channelId = "ForegroundDownloaderService" private val channelId = "ForegroundDownloaderService"
@ -68,9 +77,6 @@ class ForegroundService : Service(){
private var total = 0 //Total Downloads Requested private var total = 0 //Total Downloads Requested
private var converted = 0//Total Files Converted private var converted = 0//Total Files Converted
private var downloaded = 0//Total Files downloaded private var downloaded = 0//Total Files downloaded
private lateinit var fetch:Fetch
private lateinit var ytDownloader: YoutubeDownloader
private lateinit var downloadManager : DownloadManager
private var serviceJob = Job() private var serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val requestMap = mutableMapOf<Request, TrackDetails>() private val requestMap = mutableMapOf<Request, TrackDetails>()
@ -80,12 +86,17 @@ class ForegroundService : Service(){
private var isServiceStarted = false private var isServiceStarted = false
private var notificationLine = 0 private var notificationLine = 0
private var messageList = mutableListOf("", "", "", "") private var messageList = mutableListOf("", "", "", "")
private var cancelIntent:PendingIntent? = null private lateinit var cancelIntent:PendingIntent
private lateinit var fetch:Fetch
private lateinit var ytDownloader: YoutubeDownloader
private lateinit var downloadManager : DownloadManager
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
override fun onBind(intent: Intent): IBinder? = null override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
createNotificationChannel(channelId,"Downloader Service")
val intent = Intent( val intent = Intent(
this, this,
ForegroundService::class.java ForegroundService::class.java
@ -94,26 +105,26 @@ class ForegroundService : Service(){
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
ytDownloader = YoutubeDownloader() ytDownloader = YoutubeDownloader()
initialiseFetch() initialiseFetch()
startForeground()
} }
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
// Send a notification that service is started // Send a notification that service is started
Log.i(tag, "Service Started.") Log.i(tag, "Service Started.")
startForeground() startForeground(notificationId, getNotification())
if(intent.action == "kill") killService() when (intent.action) {
"kill" -> killService()
if(intent.action == "query"){ "query" -> {
val response = Intent().apply { val response = Intent().apply {
action = "query_result" action = "query_result"
putParcelableArrayListExtra("tracks", allTracksDetails as ArrayList<TrackDetails> ) putParcelableArrayListExtra("tracks", allTracksDetails as ArrayList<TrackDetails> )
}
sendBroadcast(response)
} }
sendBroadcast(response)
} }
val downloadObjects: ArrayList<DownloadObject>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList( val downloadObjects: ArrayList<TrackDetails>? = (intent.getParcelableArrayListExtra("object") ?: intent.extras?.getParcelableArrayList(
"object" "object"
)) ))
val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList( val imagesList: ArrayList<String>? = (intent.getStringArrayListExtra("imagesList") ?: intent.extras?.getStringArrayList(
@ -130,9 +141,9 @@ class ForegroundService : Service(){
total += downloadObjects.size total += downloadObjects.size
updateNotification() updateNotification()
it.forEach { it1 -> it.forEach { it1 ->
allTracksDetails.add(it1.trackDetails.apply { downloaded = DownloadStatus.Queued }) allTracksDetails.add(it1.apply { downloaded = DownloadStatus.Queued })
} }
downloadAllTracks(downloadObjects) downloadAllTracks(it)
} }
//Wake locks and misc tasks from here : //Wake locks and misc tasks from here :
@ -152,66 +163,96 @@ class ForegroundService : Service(){
} }
} }
private fun downloadAllTracks(downloadObjects: List<DownloadObject>){ /**
serviceScope.launch(Dispatchers.IO) { * Function To Download All Tracks Available in a List
for(downloadObj in downloadObjects){ **/
try { private fun downloadAllTracks(trackList: List<TrackDetails>) {
val video = ytDownloader.getVideo(downloadObj.ytVideoId) serviceScope.launch(Dispatchers.Default){
val format: Format? = try { trackList.forEach {
video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format if(it.downloaded == DownloadStatus.Downloaded){//Download Already Present!!
} catch (e: java.lang.IndexOutOfBoundsException) { }else {
try { if (!it.videoID.isNullOrBlank()) {//Video ID already known!
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format downloadTrack(it.videoID!!, it)
} catch (e: java.lang.IndexOutOfBoundsException) { } else {
try { val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
} catch (e: java.lang.IndexOutOfBoundsException) { youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
Log.i("YTDownloader", e.toString()) object : Callback<String> {
null override fun onResponse(
} call: Call<String>,
} response: Response<String>
} ) {
format?.let { val videoId = sortByBestMatch(
val url: String = format.url() getYTTracks(response.body().toString()),
Log.i("DHelper Link Found", url) trackName = it.title,
val request= Request(url, downloadObj.outputFile).apply{ trackArtists = it.artists,
priority = Priority.NORMAL trackDurationSec = it.durationSec
networkType = NetworkType.ALL ).keys.firstOrNull()
} Log.i("Service VideoID", videoId ?: "Not Found")
fetch.enqueue(request, if (videoId.isNullOrBlank()) sendTrackBroadcast(
{ Status.FAILED.name,
requestMap[it] = downloadObj.trackDetails it
Log.i(tag, "Enqueuing Download") )
}, else {//Found Youtube Video ID
{ downloadTrack(videoId, it)
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}") }
}
override fun onFailure(call: Call<String>, t: Throwable) {
if (t.message.toString()
.contains("Failed to connect")
) showMessage("Failed, Check Your Internet Connection!")
Log.i("YT API Req. Fail", t.message.toString())
}
} }
) )
} }
}catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString())
} }
} }
} }
} }
override fun onDestroy() { fun downloadTrack(videoID:String,track: TrackDetails){
super.onDestroy() serviceScope.launch(Dispatchers.IO) {
if(converted == total){ try {
Handler(Looper.myLooper()!!).postDelayed({ val video = ytDownloader.getVideo(videoID)
killService() val format: Format? = try {
}, 5000) video?.findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
video?.findAudioWithQuality(AudioQuality.high)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
video?.findAudioWithQuality(AudioQuality.low)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
Log.i("YTDownloader", e.toString())
null
}
}
}
format?.let {
val url: String = format.url()
Log.i("DHelper Link Found", url)
val request= Request(url, track.outputFile).apply{
priority = Priority.NORMAL
networkType = NetworkType.ALL
}
fetch.enqueue(request,
{
requestMap[it] = track
Log.i(tag, "Enqueuing Download")
},
{
Log.i(tag, "Enqueuing Error:${it.throwable.toString()}")
}
)
}
}catch (e: com.github.kiulian.downloader.YoutubeException){
Log.i("Service YT Error", e.message.toString())
}
} }
} }
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(converted == total ){
killService()
}
}
/** /**
* Fetch Listener/ Responsible for Fetch Behaviour * Fetch Listener/ Responsible for Fetch Behaviour
**/ **/
@ -220,10 +261,7 @@ class ForegroundService : Service(){
download: Download, download: Download,
waitingOnNetwork: Boolean waitingOnNetwork: Boolean
) { ) {
val intent = Intent() requestMap[download.request]?.let { sendTrackBroadcast(Status.QUEUED.name, it) }
.setAction(Status.QUEUED.name)
.putExtra("track", requestMap[download.request])
this@ForegroundService.sendBroadcast(intent)
} }
override fun onRemoved(download: Download) { override fun onRemoved(download: Download) {
@ -262,12 +300,9 @@ class ForegroundService : Service(){
track?.let{ track?.let{
allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(it.title)] = allTracksDetails[allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(it.title)] =
it.apply { downloaded = DownloadStatus.Downloading } it.apply { downloaded = DownloadStatus.Downloading }
updateNotification()
sendTrackBroadcast(Status.DOWNLOADING.name,track)
} }
updateNotification()
val intent = Intent()
.setAction(Status.DOWNLOADING.name)
.putExtra("track", track)
this@ForegroundService.sendBroadcast(intent)
} }
override fun onWaitingNetwork(download: Download) { override fun onWaitingNetwork(download: Download) {
@ -346,12 +381,12 @@ class ForegroundService : Service(){
) { ) {
val track = requestMap[download.request] val track = requestMap[download.request]
Log.i(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec") Log.i(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
val intent = Intent() val intent = Intent().apply {
.setAction("Progress") action = "Progress"
.putExtra("progress", download.progress) putExtra("progress", download.progress)
.putExtra("track", requestMap[download.request]) putExtra("track", requestMap[download.request])
this@ForegroundService.sendBroadcast(intent) }
// updateNotification() sendBroadcast(intent)
} }
} }
@ -360,16 +395,18 @@ class ForegroundService : Service(){
**/ **/
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){ fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
val uri = Uri.parse(url) val uri = Uri.parse(url)
val request = DownloadManager.Request(uri) val request = DownloadManager.Request(uri).apply {
.setAllowedNetworkTypes( setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE DownloadManager.Request.NETWORK_MOBILE
) )
.setAllowedOverRoaming(false) setAllowedOverRoaming(false)
.setTitle(track.title) setTitle(track.title)
.setDescription("Spotify Downloader Working Up here...") setDescription("Spotify Downloader Working Up here...")
.setDestinationUri(File(outputDir).toUri()) setDestinationUri(File(outputDir).toUri())
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
}
//Start Download //Start Download
val downloadID = downloadManager.enqueue(request) val downloadID = downloadManager.enqueue(request)
Log.i("DownloadManager", "Download Request Sent") Log.i("DownloadManager", "Download Request Sent")
@ -396,11 +433,7 @@ class ForegroundService : Service(){
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata) *Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/ **/
fun convertToMp3(filePath: String, track: TrackDetails){ fun convertToMp3(filePath: String, track: TrackDetails){
val intent = Intent() sendTrackBroadcast("Converting",track)
.setAction("Converting")
.putExtra("track", track)
this@ForegroundService.sendBroadcast(intent)
val m4aFile = File(filePath) val m4aFile = File(filePath)
FFmpeg.executeAsync( FFmpeg.executeAsync(
@ -441,13 +474,10 @@ class ForegroundService : Service(){
newFile.renameTo(file) newFile.renameTo(file)
converted++ converted++
updateNotification() updateNotification()
addToLibrary(file.absolutePath)
allTracksDetails.removeAt(allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(track.title)) allTracksDetails.removeAt(allTracksDetails.map{ trackDetails -> trackDetails.title}.indexOf(track.title))
//Notify Download Completed //Notify Download Completed
val intent = Intent() sendTrackBroadcast("track_download_completed",track)
.setAction("track_download_completed")
.putExtra("track", track)
this@ForegroundService.sendBroadcast(intent)
//All tasks completed (REST IN PEACE) //All tasks completed (REST IN PEACE)
if(converted == total){ if(converted == total){
onDestroy() onDestroy()
@ -460,21 +490,7 @@ class ForegroundService : Service(){
private fun updateNotification() { private fun updateNotification() {
val mNotificationManager: NotificationManager = val mNotificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, channelId) mNotificationManager.notify(notificationId, getNotification())
.setSmallIcon(R.drawable.down_arrowbw)
.setSubText("Total: $total Completed:$converted")
.setNotificationSilent()
.setStyle(
NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0])
.addLine(messageList[1])
.addLine(messageList[2])
.addLine(messageList[3])
)
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build()
mNotificationManager.notify(notificationId, notification)
} }
/** /**
@ -541,47 +557,16 @@ class ForegroundService : Service(){
isServiceStarted = false isServiceStarted = false
} }
/**
*Starting Service with Notification as Foreground!
**/
private fun startForeground() {
val channelId =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId, "Downloader Service")
} else {
// If earlier version channel ID is not used
// https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
""
}
val notification = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.down_arrowbw)
.setNotificationSilent()
.setSubText("Total: $total Completed:$converted")
.setStyle(
NotificationCompat.InboxStyle()
// .setBigContentTitle("Speed: $speed KB/s")
.addLine(messageList[0])
.addLine(messageList[1])
.addLine(messageList[2])
.addLine(messageList[3])
)
.addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
.build()
startForeground(notificationId, notification)
}
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{ private fun createNotificationChannel(channelId: String, channelName: String){
val chan = NotificationChannel( val channel = NotificationChannel(
channelId, channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT channelName, NotificationManager.IMPORTANCE_DEFAULT
) )
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan) service.createNotificationChannel(channel)
return channelId
} }
/** /**
@ -604,6 +589,15 @@ class ForegroundService : Service(){
} }
} }
/*
* Add File to Android's Media Library.
* */
private fun addToLibrary(path:String) {
Log.i(tag,"Scanning File")
MediaScannerConnection.scanFile(this,
listOf(path).toTypedArray(), null,null)
}
/** /**
* Function to fetch all Images for use in mp3 tags. * Function to fetch all Images for use in mp3 tags.
**/ **/
@ -681,6 +675,7 @@ class ForegroundService : Service(){
private fun killService() { private fun killService() {
serviceScope.launch{ serviceScope.launch{
Log.i(tag,"Killing Self")
messageList = mutableListOf("Cleaning And Exiting","","","") messageList = mutableListOf("Cleaning And Exiting","","","")
fetch.cancelAll() fetch.cancelAll()
fetch.removeAll() fetch.removeAll()
@ -696,6 +691,22 @@ class ForegroundService : Service(){
} }
} }
override fun onDestroy() {
super.onDestroy()
if(converted == total){
Handler(Looper.myLooper()!!).postDelayed({
killService()
}, 5000)
}
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if(converted == total ){
killService()
}
}
private fun initialiseFetch() { private fun initialiseFetch() {
val fetchConfiguration = val fetchConfiguration =
FetchConfiguration.Builder(this) FetchConfiguration.Builder(this)
@ -711,4 +722,28 @@ class ForegroundService : Service(){
//Starting fresh //Starting fresh
fetch.removeAll() fetch.removeAll()
} }
private fun getNotification():Notification = NotificationCompat.Builder(this, channelId).run {
setSmallIcon(R.drawable.down_arrowbw)
setSubText("Total: $total Completed:$converted")
setNotificationSilent()
setStyle(
NotificationCompat.InboxStyle().run {
addLine(messageList[0])
addLine(messageList[1])
addLine(messageList[2])
addLine(messageList[3])
}
)
addAction(R.drawable.ic_baseline_cancel_24,"Exit",cancelIntent)
build()
}
fun sendTrackBroadcast(action:String,track:TrackDetails){
val intent = Intent().apply{
setAction(action)
putExtra("track", track)
}
this@ForegroundService.sendBroadcast(intent)
}
} }

View File

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

View File

@ -18,7 +18,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext{ ext{
kotlin_version = "1.4.10" kotlin_version = "1.4.20"
navigationVersion = '2.3.0' navigationVersion = '2.3.0'
ext.hilt_version = '2.29.1-alpha' ext.hilt_version = '2.29.1-alpha'
} }
@ -28,7 +28,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.0-alpha16' classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//Safe-Args //Safe-Args
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"

View File

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