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.tonyodev.fetch2.Status
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import org.koin.android.ext.android.inject
const val disableDozeCode = 1223
@ -47,8 +46,7 @@ class MainActivity : ComponentActivity() {
private lateinit var root: SpotiFlyerRoot
private val callBacks: SpotiFlyerRootCallBacks
get() = root.callBacks
//TODO pass updates from Foreground Service
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
private val trackStatusFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver
@ -80,7 +78,7 @@ class MainActivity : ComponentActivity() {
override val database = this@MainActivity.database
override val fetchPlatformQueryResult = this@MainActivity.fetcher
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.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction(Status.COMPLETED.name)
addAction("Progress")
addAction("Converting")
addAction("track_download_completed")
}
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@ -119,18 +117,20 @@ class MainActivity : ComponentActivity() {
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { track ->
lifecycleScope.launch {
val latestMap = downloadFlow.value.apply {
val latestMap = trackStatusFlow.replayCache.getOrElse(0
) { hashMapOf() }.apply {
this[track.title] = when (intent.action) {
Status.QUEUED.name -> DownloadStatus.Queued
Status.FAILED.name -> DownloadStatus.Failed
Status.DOWNLOADING.name -> DownloadStatus.Downloading()
"Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0))
"Converting" -> DownloadStatus.Converting
"track_download_completed" -> DownloadStatus.Downloaded
Status.COMPLETED.name -> DownloadStatus.Downloaded
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 ->
Log.i("Service Response", "${list.size} Tracks Active")
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.TrackDetails
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerList {
@ -40,13 +41,18 @@ interface SpotiFlyerList {
* */
suspend fun loadImage(url:String): ImageBitmap?
/*
* Sync Tracks Statuses
* */
fun onRefreshTracksStatuses()
interface Dependencies {
val storeFactory: StoreFactory
val fetchQuery: FetchPlatformQueryResult
val dir: Dir
val link: String
val listOutput: Consumer<Output>
val downloadProgressFlow: StateFlow<HashMap<String,DownloadStatus>>
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
}
sealed class Output {
object Finished : Output()

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.database.Database
import com.shabinder.common.root.integration.SpotiFlyerRootImpl
import com.shabinder.common.utils.Consumer
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
interface SpotiFlyerRoot {
@ -32,7 +33,7 @@ interface SpotiFlyerRoot {
val database: Database
val fetchPlatformQueryResult: FetchPlatformQueryResult
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
import co.touchlab.kermit.Kermit
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.RouterState
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.Parcelize
import com.arkivanov.decompose.value.Value
import com.shabinder.common.database.getLogger
import com.shabinder.common.di.Dir
import com.shabinder.common.list.SpotiFlyerList
import com.shabinder.common.main.SpotiFlyerMain

View File

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

View File

@ -251,25 +251,30 @@ class ForegroundService : Service(),CoroutineScope{
}
override fun onCompleted(download: Download) {
launch {
val track = requestMap[download.request]
removeFromNotification("Downloading ${track?.title}")
try{
track?.let {
dir.saveFileWithMetadata(byteArrayOf(),it)
allTracksStatus[it.title] = DownloadStatus.Converting
val track = requestMap[download.request]
try{
track?.let {
val job = launch { dir.saveFileWithMetadata(byteArrayOf(),it) }
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"}
}catch (
e: KotlinNullPointerException
){
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++
requestMap.remove(download.request)
}
logger.d(tag){"${track?.title} Download Completed"}
}catch (
e: KotlinNullPointerException
){
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
downloadUsingDM(download.request.url, download.request.file, track!!)
}
downloaded++
requestMap.remove(download.request)
removeFromNotification("Downloading ${track?.title}")
}
override fun onDeleted(download: Download) {
@ -427,7 +432,7 @@ class ForegroundService : Service(),CoroutineScope{
fetch.removeAll()
updateNotification()
cleanFiles(File(dir.defaultDir()))
cleanFiles(File(dir.imageCacheDir()))
//TODO cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("","","","","")
releaseWakeLock()
serviceJob.cancel()

View File

@ -12,4 +12,6 @@ expect suspend fun downloadTracks(
list: List<TrackDetails>,
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
)
)
expect fun queryActiveTracks()

View File

@ -242,7 +242,7 @@ class YoutubeMusic constructor(
val avgMatch = (artistMatch + durationMatch)/2
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()
}

View File

@ -22,7 +22,9 @@ actual fun giveDonation(){
//TODO
}
val DownloadProgressFlow: MutableStateFlow<HashMap<String,DownloadStatus>> = MutableStateFlow(hashMapOf())
actual fun queryActiveTracks(){}
val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = MutableSharedFlow(1)
actual suspend fun downloadTracks(
list: List<TrackDetails>,
@ -36,7 +38,8 @@ actual suspend fun downloadTracks(
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = getYTIDBestMatch(searchQuery,it)
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
downloadTrack(videoId, it,saveFileWithMetaData)
}
@ -59,14 +62,17 @@ suspend fun downloadTrack(
downloadFile(url).collect {
when(it){
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 -> {
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
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) })
}
}
}