MutableSharedFlow,Active Tracks Query ,etc fixes

This commit is contained in:
shabinder 2021-02-23 03:12:58 +05:30
parent 1b58bbfcf0
commit f7e71da827
13 changed files with 92 additions and 51 deletions

View File

@ -33,8 +33,7 @@ import com.shabinder.common.ui.colorOffWhite
import com.shabinder.database.Database import com.shabinder.database.Database
import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.Status
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.StateFlow
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
const val disableDozeCode = 1223 const val disableDozeCode = 1223
@ -47,8 +46,7 @@ class MainActivity : ComponentActivity() {
private lateinit var root: SpotiFlyerRoot private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks private val callBacks: SpotiFlyerRootCallBacks
get() = root.callBacks get() = root.callBacks
//TODO pass updates from Foreground Service private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
private lateinit var updateUIReceiver: BroadcastReceiver private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver private lateinit var queryReceiver: BroadcastReceiver
@ -80,7 +78,7 @@ class MainActivity : ComponentActivity() {
override val database = this@MainActivity.database override val database = this@MainActivity.database
override val fetchPlatformQueryResult = this@MainActivity.fetcher override val fetchPlatformQueryResult = this@MainActivity.fetcher
override val directories: Dir = this@MainActivity.dir override val directories: Dir = this@MainActivity.dir
override val downloadProgressReport: StateFlow<HashMap<String, DownloadStatus>> = downloadFlow override val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>> = trackStatusFlow
} }
) )
@ -108,9 +106,9 @@ class MainActivity : ComponentActivity() {
addAction(Status.QUEUED.name) addAction(Status.QUEUED.name)
addAction(Status.FAILED.name) addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name) addAction(Status.DOWNLOADING.name)
addAction(Status.COMPLETED.name)
addAction("Progress") addAction("Progress")
addAction("Converting") addAction("Converting")
addAction("track_download_completed")
} }
updateUIReceiver = object : BroadcastReceiver() { updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
@ -119,18 +117,20 @@ class MainActivity : ComponentActivity() {
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track") val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { track -> trackDetails?.let { track ->
lifecycleScope.launch { lifecycleScope.launch {
val latestMap = downloadFlow.value.apply { val latestMap = trackStatusFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply {
this[track.title] = when (intent.action) { this[track.title] = when (intent.action) {
Status.QUEUED.name -> DownloadStatus.Queued Status.QUEUED.name -> DownloadStatus.Queued
Status.FAILED.name -> DownloadStatus.Failed Status.FAILED.name -> DownloadStatus.Failed
Status.DOWNLOADING.name -> DownloadStatus.Downloading() Status.DOWNLOADING.name -> DownloadStatus.Downloading()
"Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0)) "Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0))
"Converting" -> DownloadStatus.Converting "Converting" -> DownloadStatus.Converting
"track_download_completed" -> DownloadStatus.Downloaded Status.COMPLETED.name -> DownloadStatus.Downloaded
else -> DownloadStatus.NotDownloaded else -> DownloadStatus.NotDownloaded
} }
} }
downloadFlow.emit(latestMap) trackStatusFlow.emit(latestMap)
Log.i("Track Update",track.title + track.downloaded.toString())
} }
} }
} }
@ -146,7 +146,7 @@ class MainActivity : ComponentActivity() {
trackList?.let { list -> trackList?.let { list ->
Log.i("Service Response", "${list.size} Tracks Active") Log.i("Service Response", "${list.size} Tracks Active")
lifecycleScope.launch { lifecycleScope.launch {
downloadFlow.emit(list) trackStatusFlow.emit(list)
} }
} }
} }

View File

@ -14,6 +14,7 @@ import com.shabinder.common.utils.Consumer
import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerList { interface SpotiFlyerList {
@ -40,13 +41,18 @@ interface SpotiFlyerList {
* */ * */
suspend fun loadImage(url:String): ImageBitmap? suspend fun loadImage(url:String): ImageBitmap?
/*
* Sync Tracks Statuses
* */
fun onRefreshTracksStatuses()
interface Dependencies { interface Dependencies {
val storeFactory: StoreFactory val storeFactory: StoreFactory
val fetchQuery: FetchPlatformQueryResult val fetchQuery: FetchPlatformQueryResult
val dir: Dir val dir: Dir
val link: String val link: String
val listOutput: Consumer<Output> val listOutput: Consumer<Output>
val downloadProgressFlow: StateFlow<HashMap<String,DownloadStatus>> val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
} }
sealed class Output { sealed class Output {
object Finished : Output() object Finished : Output()

View File

@ -42,5 +42,9 @@ internal class SpotiFlyerListImpl(
listOutput.callback(SpotiFlyerList.Output.Finished) listOutput.callback(SpotiFlyerList.Output.Finished)
} }
override fun onRefreshTracksStatuses() {
store.accept(Intent.RefreshTracksStatuses)
}
override suspend fun loadImage(url: String): ImageBitmap? = dir.loadImage(url) override suspend fun loadImage(url: String): ImageBitmap? = dir.loadImage(url)
} }

View File

@ -10,5 +10,6 @@ internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> {
data class SearchLink(val link: String): Intent() data class SearchLink(val link: String): Intent()
data class StartDownload(val track:TrackDetails): Intent() data class StartDownload(val track:TrackDetails): Intent()
data class StartDownloadAll(val trackList: List<TrackDetails>): Intent() data class StartDownloadAll(val trackList: List<TrackDetails>): Intent()
object RefreshTracksStatuses: Intent()
} }
} }

View File

@ -4,15 +4,18 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import com.arkivanov.mvikotlin.core.store.* import com.arkivanov.mvikotlin.core.store.*
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.downloadTracks import com.shabinder.common.di.downloadTracks
import com.shabinder.common.di.queryActiveTracks
import com.shabinder.common.list.SpotiFlyerList.State import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.ui.showPopUpMessage import com.shabinder.common.ui.showPopUpMessage
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -21,8 +24,9 @@ internal class SpotiFlyerListStoreProvider(
private val storeFactory: StoreFactory, private val storeFactory: StoreFactory,
private val fetchQuery: FetchPlatformQueryResult, private val fetchQuery: FetchPlatformQueryResult,
private val link: String, private val link: String,
private val downloadProgressFlow: StateFlow<HashMap<String, DownloadStatus>> private val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
) { ) {
val logger = getLogger()
fun provide(): SpotiFlyerListStore = fun provide(): SpotiFlyerListStore =
object : SpotiFlyerListStore, Store<Intent, State, Nothing> by storeFactory.create( object : SpotiFlyerListStore, Store<Intent, State, Nothing> by storeFactory.create(
name = "SpotiFlyerListStore", name = "SpotiFlyerListStore",
@ -42,8 +46,10 @@ internal class SpotiFlyerListStoreProvider(
override suspend fun executeAction(action: Unit, getState: () -> State) { override suspend fun executeAction(action: Unit, getState: () -> State) {
executeIntent(Intent.SearchLink(link),getState) executeIntent(Intent.SearchLink(link),getState)
executeIntent(Intent.RefreshTracksStatuses,getState)
downloadProgressFlow.collectLatest { map -> downloadProgressFlow.collectLatest { map ->
logger.d(map.size.toString(),"ListStore: flow Updated")
val updatedTrackList = getState().trackList.updateTracksStatuses(map) val updatedTrackList = getState().trackList.updateTracksStatuses(map)
if(updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList)) if(updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
} }
@ -53,7 +59,7 @@ internal class SpotiFlyerListStoreProvider(
when (intent) { when (intent) {
is Intent.SearchLink -> fetchQuery.query(link)?.let{ result -> is Intent.SearchLink -> fetchQuery.query(link)?.let{ result ->
result.trackList = result.trackList.toMutableList() result.trackList = result.trackList.toMutableList()
dispatch((Result.ResultFetched(result,result.trackList.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))) dispatch((Result.ResultFetched(result,result.trackList.toMutableList().updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()}))))
} }
is Intent.StartDownloadAll -> { is Intent.StartDownloadAll -> {
@ -68,12 +74,13 @@ internal class SpotiFlyerListStoreProvider(
} }
it it
} }
dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value))) dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))
} }
is Intent.StartDownload -> { is Intent.StartDownload -> {
downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata) downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued })) dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued }))
} }
is Intent.RefreshTracksStatuses -> queryActiveTracks()
} }
} }
} }
@ -93,18 +100,18 @@ internal class SpotiFlyerListStoreProvider(
return this return this
} }
} }
} private fun MutableList<TrackDetails>.updateTracksStatuses(map:HashMap<String,DownloadStatus>):SnapshotStateList<TrackDetails>{
private fun MutableList<TrackDetails>.updateTracksStatuses(map:HashMap<String,DownloadStatus>):SnapshotStateList<TrackDetails>{
val titleList = this.map { it.title } val titleList = this.map { it.title }
val newStateList = mutableStateListOf<TrackDetails>() val newStateList = mutableStateListOf<TrackDetails>()
for(newTrack in map){ for(newTrack in map){
titleList.indexOf(newTrack.key).let { position -> titleList.indexOf(newTrack.key).let { position ->
this.getOrNull(position)?.apply { downloaded = newTrack.value }?.also { updatedTrack -> this.getOrNull(position)?.apply { downloaded = newTrack.value }?.also { updatedTrack ->
this[position] = updatedTrack this[position] = updatedTrack
logger.d(updatedTrack.toString(),"List Store Track Update")
} }
} }
} }
newStateList.addAll(this) newStateList.addAll(this)
return newStateList return newStateList
}
} }

View File

@ -53,7 +53,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain){
when(model.selectedCategory){ when(model.selectedCategory){
HomeCategory.About -> AboutColumn() HomeCategory.About -> AboutColumn()
HomeCategory.History -> HistoryColumn( HomeCategory.History -> HistoryColumn(
model.records, model.records.sortedByDescending { it.id },
component::loadImage, component::loadImage,
component::onLinkSearch component::onLinkSearch
) )

View File

@ -14,6 +14,7 @@ import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.database.Database import com.shabinder.database.Database
import com.shabinder.common.root.integration.SpotiFlyerRootImpl import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.common.utils.Consumer import com.shabinder.common.utils.Consumer
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerRoot { interface SpotiFlyerRoot {
@ -32,7 +33,7 @@ interface SpotiFlyerRoot {
val database: Database val database: Database
val fetchPlatformQueryResult: FetchPlatformQueryResult val fetchPlatformQueryResult: FetchPlatformQueryResult
val directories: Dir val directories: Dir
val downloadProgressReport: StateFlow<HashMap<String,DownloadStatus>> val downloadProgressReport: MutableSharedFlow<HashMap<String, DownloadStatus>>
} }
} }

View File

@ -1,5 +1,6 @@
package com.shabinder.common.root.integration package com.shabinder.common.root.integration
import co.touchlab.kermit.Kermit
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.RouterState import com.arkivanov.decompose.RouterState
import com.arkivanov.decompose.pop import com.arkivanov.decompose.pop
@ -8,6 +9,7 @@ import com.arkivanov.decompose.router
import com.arkivanov.decompose.statekeeper.Parcelable import com.arkivanov.decompose.statekeeper.Parcelable
import com.arkivanov.decompose.statekeeper.Parcelize import com.arkivanov.decompose.statekeeper.Parcelize
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.list.SpotiFlyerList import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain

View File

@ -41,6 +41,13 @@ actual fun giveDonation(){
//TODO //TODO
} }
actual fun queryActiveTracks() {
val serviceIntent = Intent(appContext, ForegroundService::class.java).apply {
action = "query"
}
ContextCompat.startForegroundService(appContext, serviceIntent)
}
actual suspend fun downloadTracks( actual suspend fun downloadTracks(
list: List<TrackDetails>, list: List<TrackDetails>,
getYTIDBestMatch:suspend (String,TrackDetails)->String?, getYTIDBestMatch:suspend (String,TrackDetails)->String?,

View File

@ -251,13 +251,18 @@ class ForegroundService : Service(),CoroutineScope{
} }
override fun onCompleted(download: Download) { override fun onCompleted(download: Download) {
launch {
val track = requestMap[download.request] val track = requestMap[download.request]
removeFromNotification("Downloading ${track?.title}")
try{ try{
track?.let { track?.let {
dir.saveFileWithMetadata(byteArrayOf(),it) val job = launch { dir.saveFileWithMetadata(byteArrayOf(),it) }
allTracksStatus[it.title] = DownloadStatus.Converting allTracksStatus[it.title] = DownloadStatus.Converting
sendTrackBroadcast("Converting",it)
addToNotification("Processing ${it.title}")
job.invokeOnCompletion { _ ->
converted++
sendTrackBroadcast(Status.COMPLETED.name,it)
removeFromNotification("Processing ${it.title}")
}
} }
logger.d(tag){"${track?.title} Download Completed"} logger.d(tag){"${track?.title} Download Completed"}
}catch ( }catch (
@ -266,10 +271,10 @@ class ForegroundService : Service(),CoroutineScope{
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"} logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
logger.d(tag){"${track?.title} Requesting Download thru Android DM"} logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
downloadUsingDM(download.request.url, download.request.file, track!!) downloadUsingDM(download.request.url, download.request.file, track!!)
}
downloaded++ downloaded++
requestMap.remove(download.request) requestMap.remove(download.request)
} removeFromNotification("Downloading ${track?.title}")
}
} }
override fun onDeleted(download: Download) { override fun onDeleted(download: Download) {
@ -427,7 +432,7 @@ class ForegroundService : Service(),CoroutineScope{
fetch.removeAll() fetch.removeAll()
updateNotification() updateNotification()
cleanFiles(File(dir.defaultDir())) cleanFiles(File(dir.defaultDir()))
cleanFiles(File(dir.imageCacheDir())) //TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("","","","","") messageList = mutableListOf("","","","","")
releaseWakeLock() releaseWakeLock()
serviceJob.cancel() serviceJob.cancel()

View File

@ -13,3 +13,5 @@ expect suspend fun downloadTracks(
getYTIDBestMatch:suspend (String,TrackDetails)->String?, getYTIDBestMatch:suspend (String,TrackDetails)->String?,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
) )
expect fun queryActiveTracks()

View File

@ -242,7 +242,7 @@ class YoutubeMusic constructor(
val avgMatch = (artistMatch + durationMatch)/2 val avgMatch = (artistMatch + durationMatch)/2
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt() linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
} }
logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"} //logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap() return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
} }

View File

@ -22,7 +22,9 @@ actual fun giveDonation(){
//TODO //TODO
} }
val DownloadProgressFlow: MutableStateFlow<HashMap<String,DownloadStatus>> = MutableStateFlow(hashMapOf()) actual fun queryActiveTracks(){}
val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = MutableSharedFlow(1)
actual suspend fun downloadTracks( actual suspend fun downloadTracks(
list: List<TrackDetails>, list: List<TrackDetails>,
@ -36,7 +38,8 @@ actual suspend fun downloadTracks(
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = getYTIDBestMatch(searchQuery,it) val videoId = getYTIDBestMatch(searchQuery,it)
if (videoId.isNullOrBlank()) { if (videoId.isNullOrBlank()) {
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(it.title,DownloadStatus.Failed) }) DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) })
} else {//Found Youtube Video ID } else {//Found Youtube Video ID
downloadTrack(videoId, it,saveFileWithMetaData) downloadTrack(videoId, it,saveFileWithMetaData)
} }
@ -59,14 +62,17 @@ suspend fun downloadTrack(
downloadFile(url).collect { downloadFile(url).collect {
when(it){ when(it){
is DownloadResult.Error -> { is DownloadResult.Error -> {
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Failed) }) DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Failed) })
} }
is DownloadResult.Progress -> { is DownloadResult.Progress -> {
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) }) DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) })
} }
is DownloadResult.Success -> {//Todo clear map is DownloadResult.Success -> {//Todo clear map
saveFileWithMetaData(it.byteArray,trackDetails) saveFileWithMetaData(it.byteArray,trackDetails)
DownloadProgressFlow.emit(DownloadProgressFlow.value.apply { set(trackDetails.title,DownloadStatus.Downloaded) }) DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloaded) })
} }
} }
} }