WIP:Download Impl

This commit is contained in:
shabinder 2021-02-15 03:34:37 +05:30
parent 5ceb58e6de
commit 50fa91fbca
20 changed files with 158 additions and 57 deletions

View File

@ -20,6 +20,7 @@ internal class SpotiFlyerListImpl(
private val store =
instanceKeeper.getStore {
SpotiFlyerListStoreProvider(
dir = this.dir,
storeFactory = storeFactory,
fetchQuery = fetchQuery,
link = link

View File

@ -2,6 +2,7 @@ package com.shabinder.common.list.store
import com.arkivanov.mvikotlin.core.store.*
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.downloadTracks
import com.shabinder.common.list.SpotiFlyerList.State
@ -9,8 +10,10 @@ 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
internal class SpotiFlyerListStoreProvider(
private val dir: Dir,
private val storeFactory: StoreFactory,
private val fetchQuery: FetchPlatformQueryResult,
private val link: String
@ -45,8 +48,8 @@ internal class SpotiFlyerListStoreProvider(
is Intent.StartDownloadAll -> {
val finalList =
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
if (finalList.isNullOrEmpty()) //TODO showDialog("All Songs are Processed")
else downloadTracks(finalList)
if (finalList.isNullOrEmpty()) showPopUpMessage("All Songs are Processed")
else downloadTracks(finalList,fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
val list = intent.trackList.map {
if (it.downloaded == DownloadStatus.NotDownloaded) {

View File

@ -25,9 +25,11 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.shabinder.common.di.giveDonation
import com.shabinder.common.models.DownloadRecord
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
import com.shabinder.common.di.openPlatform
import com.shabinder.common.di.shareApp
import com.shabinder.common.ui.*
import com.shabinder.common.ui.SpotiFlyerTypography
@ -73,10 +75,9 @@ fun HomeTabBar(
)
}
@Suppress("USELESS_CAST")//Showing Error in Latest Android Studio Canary
TabRow(
selectedTabIndex = selectedIndex,
indicator = indicator as @Composable (List<TabPosition>) -> Unit,
indicator = indicator,
modifier = modifier,
) {
categories.forEachIndexed { index, category ->
@ -245,51 +246,44 @@ fun AboutColumn(modifier: Modifier = Modifier) {
)
}
}
/*Row(
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = { startPayment(mainActivity) }),
.clickable(onClick = { giveDonation() }),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.MailOutline.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Icon(Icons.Rounded.MailOutline,"Support Developer")
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.donate),
text = "Donate",
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.donate_subtitle),
text = "If you think I deserve to get paid for my work, you can leave me some money here.",
style = SpotiFlyerTypography.subtitle2
)
}
}*/
/*Row(
}
Row(
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
.clickable(onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
ctx.startActivity(shareIntent)
shareApp()
}),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Rounded.Share.copy(defaultHeight = 32.dp,defaultWidth = 32.dp))
Icon(Icons.Rounded.Share,"Share SpotiFlyer App")
Spacer(modifier = Modifier.padding(start = 16.dp))
Column {
Text(
text = stringResource(R.string.share),
text = "Share",
style = SpotiFlyerTypography.h6
)
Text(
text = stringResource(R.string.share_subtitle),
text = "Share this app with your friends and family.",
style = SpotiFlyerTypography.subtitle2
)
}
}*/
}
}
}
}

View File

@ -34,15 +34,16 @@ data class TrackDetails(
var source: Source,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var progress: Int = 2,//2 for visual progress bar hint
var outputFile: String,
var outputFilePath: String,
var videoID:String? = null
)
enum class DownloadStatus{
Downloaded,
Downloading,
Queued,
NotDownloaded,
Converting,
Failed
@Serializable
sealed class DownloadStatus {
object Downloaded :DownloadStatus()
data class Downloading(val progress: Int = 0):DownloadStatus()
object Queued :DownloadStatus()
object NotDownloaded :DownloadStatus()
object Converting :DownloadStatus()
object Failed :DownloadStatus()
}

View File

@ -22,13 +22,24 @@ actual fun openPlatform(packageID:String, platformLink:String){
}
actual fun shareApp(){
//TODO
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
appContext.startActivity(shareIntent)
}
actual fun giveDonation(){
//TODO
}
actual fun downloadTracks(list: List<TrackDetails>){
actual suspend fun downloadTracks(
list: List<TrackDetails>,
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
){
//TODO
}

View File

@ -32,7 +32,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String){
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
val id3v2Tag = ID3v24Tag().apply {
artist = track.artists.joinToString(",")
title = track.title
@ -50,7 +50,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String
fis.close()
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
this.id3v2Tag = id3v2Tag
saveFile(filePath)
saveFile(track.outputFilePath)
}catch (e: java.io.FileNotFoundException){
try {
//Image Still Not Downloaded!
@ -61,7 +61,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String
is DownloadResult.Success -> {
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
this.id3v2Tag = id3v2Tag
saveFile(filePath)
saveFile(track.outputFilePath)
}
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
}

View File

@ -125,7 +125,7 @@ actual class YoutubeProvider actual constructor(
else {
DownloadStatus.NotDownloaded
},
outputFile = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
videoID = it.videoId()
)
}
@ -195,7 +195,7 @@ actual class YoutubeProvider actual constructor(
else {
DownloadStatus.NotDownloaded
},
outputFile = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
videoID = searchId
)
)

View File

@ -33,7 +33,7 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { SpotifyProvider(get(),get(),get(),get()) }
single { GaanaProvider(get(),get(),get(),get()) }
single { YoutubeProvider(get(),get(),get(),get()) }
single { FetchPlatformQueryResult(get(),get(),get(),get()) }
single { FetchPlatformQueryResult(get(),get(),get(),get(),get()) }
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
}

View File

@ -2,6 +2,7 @@ package com.shabinder.common.di
import androidx.compose.ui.graphics.ImageBitmap
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.TrackDetails
import io.ktor.client.request.*
@ -13,7 +14,7 @@ import kotlinx.coroutines.flow.flow
import kotlin.math.roundToInt
expect class Dir(
logger: Kermit,
logger: Kermit
) {
fun isPresent(path:String):Boolean
fun fileSeparator(): String
@ -23,7 +24,7 @@ expect class Dir(
suspend fun cacheImage(image: Any,path: String) // in Android = ImageBitmap, Desktop = BufferedImage
suspend fun loadImage(url:String): ImageBitmap?
suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails)
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
}
suspend fun downloadFile(url: String): Flow<DownloadResult> {

View File

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

View File

@ -4,6 +4,7 @@ import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.database.Database
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -12,6 +13,7 @@ class FetchPlatformQueryResult(
private val gaanaProvider: GaanaProvider,
private val spotifyProvider: SpotifyProvider,
private val youtubeProvider: YoutubeProvider,
val youtubeMusic: YoutubeMusic,
private val database: Database
) {
private val db:DownloadRecordDatabaseQueries

View File

@ -1,5 +1,7 @@
package com.shabinder.common.di
/**
* Removing Illegal Chars from File Name
* **/

View File

@ -220,7 +220,7 @@ class GaanaProvider(
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana,
albumArtURL = it.artworkLink,
outputFile = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir(),".m4a")
outputFilePath = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir(),".m4a")
)
}
}

View File

@ -23,7 +23,6 @@ import com.shabinder.common.di.TokenStore
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.kotlinxSerializer
import com.shabinder.common.di.spotify.SpotifyRequests
import com.shabinder.common.di.spotify.authenticateSpotify
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.spotify.Album
@ -35,7 +34,6 @@ import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -273,7 +271,7 @@ class SpotifyProvider(
downloaded = it.downloaded,
source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFile = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir(),".m4a")
outputFilePath = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir(),".m4a")
)
}
}

View File

@ -1,6 +1,7 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Logger
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.*
@ -16,6 +17,15 @@ class YoutubeMusic constructor(
private val httpClient:HttpClient,
) {
private val tag = "YT Music"
suspend fun getYTIDBestMatch(query: String,trackDetails: TrackDetails):String?{
return sortByBestMatch(
getYTTracks(query),
trackName = trackDetails.title,
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
}
suspend fun getYTTracks(query: String):List<YoutubeTrack>{
val youtubeTracks = mutableListOf<YoutubeTrack>()

View File

@ -1,6 +1,16 @@
package com.shabinder.common.di
import com.github.kiulian.downloader.YoutubeDownloader
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
actual fun openPlatform(packageID:String, platformLink:String){
//TODO
@ -14,6 +24,71 @@ actual fun giveDonation(){
//TODO
}
actual fun downloadTracks(list: List<TrackDetails>){
//TODO
val DownloadProgressFlow = MutableStateFlow(Pair<String,DownloadStatus>("",DownloadStatus.Queued))
actual suspend fun downloadTracks(
list: List<TrackDetails>,
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
){
list.forEach {
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
downloadTrack(it.videoID!!, it,saveFileWithMetaData)
} else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = getYTIDBestMatch(searchQuery,it)
if (videoId.isNullOrBlank()) {
DownloadProgressFlow.emit(Pair(it.title,DownloadStatus.Failed))
} else {//Found Youtube Video ID
downloadTrack(videoId, it,saveFileWithMetaData)
}
}
}
}
val ytDownloader = YoutubeDownloader()
suspend fun downloadTrack(
videoID: String,
trackDetails: TrackDetails,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
) {
try {
val audioData = ytDownloader.getVideo(videoID).getData()
audioData?.let { format ->
val url: String = format.url()
downloadFile(url).collect {
when(it){
is DownloadResult.Error -> {
//TODO()
}
is DownloadResult.Progress -> {
DownloadProgressFlow.emit(Pair(trackDetails.title,DownloadStatus.Downloading(it.progress)))
}
is DownloadResult.Success -> {
saveFileWithMetaData(it.byteArray,trackDetails)
DownloadProgressFlow.emit(Pair(trackDetails.title,DownloadStatus.Downloaded))
}
}
}
}
}catch (e: java.lang.Exception){
e.printStackTrace()
}
}
fun YoutubeVideo.getData(): Format?{
return try {
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.high)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.low)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
null
}
}
}
}

View File

@ -61,16 +61,15 @@ actual class Dir actual constructor(private val logger: Kermit) {
@Suppress("BlockingMethodInNonBlockingContext")
actual suspend fun saveFileWithMetadata(
mp3ByteArray: ByteArray,
path: String,
trackDetails: TrackDetails
) {
val file = File(path)
val file = File(trackDetails.outputFilePath)
file.writeBytes(mp3ByteArray)
Mp3File(file)
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails,path)
.setId3v2TagsAndSaveFile(trackDetails)
}
actual suspend fun loadImage(url: String): ImageBitmap? {

View File

@ -33,7 +33,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String){
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
val id3v2Tag = ID3v24Tag().apply {
artist = track.artists.joinToString(",")
title = track.title
@ -51,7 +51,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String
fis.close()
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
this.id3v2Tag = id3v2Tag
saveFile(filePath)
saveFile(track.outputFilePath)
}catch (e: java.io.FileNotFoundException){
try {
//Image Still Not Downloaded!
@ -62,7 +62,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails, filePath:String
is DownloadResult.Success -> {
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
this.id3v2Tag = id3v2Tag
saveFile(filePath)
saveFile(track.outputFilePath)
}
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
}

View File

@ -126,7 +126,7 @@ actual class YoutubeProvider actual constructor(
else {
DownloadStatus.NotDownloaded
},
outputFile = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
videoID = it.videoId()
)
}
@ -196,7 +196,7 @@ actual class YoutubeProvider actual constructor(
else {
DownloadStatus.NotDownloaded
},
outputFile = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
videoID = searchId
)
)

View File

@ -21,6 +21,6 @@
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:fillColor="#FFFFFF"
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</vector>