mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 09:04:32 +01:00
fun's: Mp3 Tagging File Downloading,etc
This commit is contained in:
parent
c7c61e51d6
commit
3ae3b404b1
@ -5,12 +5,14 @@ import androidx.compose.foundation.layout.preferredHeight
|
|||||||
import androidx.compose.foundation.layout.preferredWidth
|
import androidx.compose.foundation.layout.preferredWidth
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import com.shabinder.common.database.appContext
|
||||||
import dev.chrisbanes.accompanist.coil.CoilImage
|
import dev.chrisbanes.accompanist.coil.CoilImage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -29,3 +31,16 @@ actual fun ImageLoad(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun Toast(
|
||||||
|
text: String,
|
||||||
|
visibility: MutableState<Boolean>,
|
||||||
|
duration: ToastDuration
|
||||||
|
){
|
||||||
|
//We Have Android's Implementation of Toast so its just Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun showPopUpMessage(text: String){
|
||||||
|
android.widget.Toast.makeText(appContext,text, android.widget.Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
@ -11,4 +11,6 @@ expect fun ImageLoad(
|
|||||||
loadingResource: ImageBitmap? = null,
|
loadingResource: ImageBitmap? = null,
|
||||||
errorResource: ImageBitmap? = null,
|
errorResource: ImageBitmap? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expect fun showPopUpMessage(text: String)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.shabinder.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
||||||
|
enum class ToastDuration(val value: Int) {
|
||||||
|
Short(1000), Long(3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun Toast(
|
||||||
|
text: String,
|
||||||
|
visibility: MutableState<Boolean> = mutableStateOf(false),
|
||||||
|
duration: ToastDuration = ToastDuration.Long
|
||||||
|
)
|
@ -0,0 +1,70 @@
|
|||||||
|
package com.shabinder.common.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.preferredSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private val message: MutableState<String> = mutableStateOf("")
|
||||||
|
private val state: MutableState<Boolean> = mutableStateOf(false)
|
||||||
|
|
||||||
|
actual fun showPopUpMessage(text: String) {
|
||||||
|
message.value = text
|
||||||
|
state.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isShown: Boolean = false
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun Toast(
|
||||||
|
text: String,
|
||||||
|
visibility: MutableState<Boolean>,
|
||||||
|
duration: ToastDuration
|
||||||
|
) {
|
||||||
|
if (isShown) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibility.value) {
|
||||||
|
isShown = true
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(bottom = 20.dp),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.preferredSize(300.dp, 70.dp),
|
||||||
|
color = Color(23, 23, 23),
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = Color(210, 210, 210)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
GlobalScope.launch {
|
||||||
|
delay(duration.value.toLong())
|
||||||
|
isShown = false
|
||||||
|
visibility.value = false
|
||||||
|
}
|
||||||
|
onDispose { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
sealed class DownloadResult {
|
||||||
|
|
||||||
|
data class Error(val message: String, val cause: Exception? = null) : DownloadResult()
|
||||||
|
|
||||||
|
data class Progress(val progress: Int): DownloadResult()
|
||||||
|
|
||||||
|
data class Success(val byteArray: ByteArray) : DownloadResult() {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other == null || this::class != other::class) return false
|
||||||
|
|
||||||
|
other as Success
|
||||||
|
|
||||||
|
if (!byteArray.contentEquals(other.byteArray)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return byteArray.contentHashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,10 +24,8 @@ kotlin {
|
|||||||
api(Koin.test)
|
api(Koin.test)
|
||||||
|
|
||||||
api(Extras.kermit)
|
api(Extras.kermit)
|
||||||
api(Extras.jsonKlaxon)
|
|
||||||
api(Extras.youtubeDownloader)
|
api(Extras.youtubeDownloader)
|
||||||
//api(Extras.fuzzyWuzzy)
|
api(Extras.mp3agic)
|
||||||
//api("com.github.willowtreeapps:fuzzywuzzy-kotlin:v0.1.1")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.database.appContext
|
import com.shabinder.common.database.appContext
|
||||||
import java.io.File
|
import java.io.*
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
actual fun openPlatform(platformID:String ,platformLink:String){
|
actual fun openPlatform(platformID:String ,platformLink:String){
|
||||||
//TODO
|
//TODO
|
||||||
@ -20,24 +22,4 @@ actual fun giveDonation(){
|
|||||||
|
|
||||||
actual fun downloadTracks(list: List<TrackDetails>){
|
actual fun downloadTracks(list: List<TrackDetails>){
|
||||||
//TODO
|
//TODO
|
||||||
}
|
|
||||||
|
|
||||||
actual open class Dir actual constructor(logger: Kermit) {
|
|
||||||
|
|
||||||
private val context:Context
|
|
||||||
get() = appContext
|
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
|
||||||
|
|
||||||
actual fun imageDir(): String = context.cacheDir.absolutePath + File.separator
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
actual fun defaultDir(): String =
|
|
||||||
Environment.getExternalStorageDirectory().toString() + File.separator +
|
|
||||||
Environment.DIRECTORY_MUSIC + File.separator +
|
|
||||||
"SpotiFlyer"+ File.separator
|
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
|
||||||
actual fun createDirectory(dirPath: String) {
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Environment
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.mpatric.mp3agic.Mp3File
|
||||||
|
import com.shabinder.common.database.appContext
|
||||||
|
import java.io.*
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
actual open class Dir actual constructor(
|
||||||
|
private val logger: Kermit
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context: Context
|
||||||
|
get() = appContext
|
||||||
|
|
||||||
|
actual fun fileSeparator(): String = File.separator
|
||||||
|
|
||||||
|
actual fun imageCacheDir(): String = context.cacheDir.absolutePath + File.separator
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
actual fun defaultDir(): String =
|
||||||
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
|
Environment.DIRECTORY_MUSIC + File.separator +
|
||||||
|
"SpotiFlyer"+ File.separator
|
||||||
|
|
||||||
|
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||||
|
|
||||||
|
actual fun createDirectory(dirPath: String) {
|
||||||
|
val yourAppDir = File(dirPath)
|
||||||
|
|
||||||
|
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||||
|
{ // create empty directory
|
||||||
|
if (yourAppDir.mkdirs())
|
||||||
|
{logger.i{"$dirPath created"}}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.e{"Unable to create Dir: $dirPath!"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.i { "$dirPath already exists" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun clearCache(){
|
||||||
|
File(imageCacheDir()).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun cacheImage(picture: Picture) {
|
||||||
|
try {
|
||||||
|
val path = imageCacheDir() + picture.name
|
||||||
|
FileOutputStream(path).use { out ->
|
||||||
|
picture.image.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
val bw =
|
||||||
|
BufferedWriter(
|
||||||
|
OutputStreamWriter(
|
||||||
|
FileOutputStream(path + cacheImagePostfix()), StandardCharsets.UTF_8
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
bw.write(picture.source)
|
||||||
|
bw.write("\r\n${picture.width}")
|
||||||
|
bw.write("\r\n${picture.height}")
|
||||||
|
bw.close()
|
||||||
|
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
actual suspend fun saveFileWithMetadata(
|
||||||
|
mp3ByteArray: ByteArray,
|
||||||
|
path: String,
|
||||||
|
trackDetails: TrackDetails
|
||||||
|
) {
|
||||||
|
val file = File(path)
|
||||||
|
file.writeBytes(mp3ByteArray)
|
||||||
|
|
||||||
|
Mp3File(file)
|
||||||
|
.removeAllTags()
|
||||||
|
.setId3v1Tags(trackDetails)
|
||||||
|
.setId3v2TagsAndSaveFile(trackDetails,path)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import com.mpatric.mp3agic.ID3v1Tag
|
||||||
|
import com.mpatric.mp3agic.ID3v24Tag
|
||||||
|
import com.mpatric.mp3agic.Mp3File
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
fun Mp3File.removeAllTags(): Mp3File {
|
||||||
|
removeId3v1Tag()
|
||||||
|
removeId3v2Tag()
|
||||||
|
removeCustomTag()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifying Mp3 with MetaData!
|
||||||
|
**/
|
||||||
|
fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
||||||
|
val id3v1Tag = ID3v1Tag().apply {
|
||||||
|
artist = track.artists.joinToString(",")
|
||||||
|
title = track.title
|
||||||
|
album = track.albumName
|
||||||
|
year = track.year
|
||||||
|
comment = "Genres:${track.comment}"
|
||||||
|
}
|
||||||
|
this.id3v1Tag = id3v1Tag
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails,filePath:String){
|
||||||
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
|
artist = track.artists.joinToString(",")
|
||||||
|
title = track.title
|
||||||
|
album = track.albumName
|
||||||
|
year = track.year
|
||||||
|
comment = "Genres:${track.comment}"
|
||||||
|
lyrics = "Gonna Implement Soon"
|
||||||
|
url = track.trackUrl
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
val art = File(track.albumArtPath)
|
||||||
|
val bytesArray = ByteArray(art.length().toInt())
|
||||||
|
val fis = FileInputStream(art)
|
||||||
|
fis.read(bytesArray) //read file into bytes[]
|
||||||
|
fis.close()
|
||||||
|
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||||
|
this.id3v2Tag = id3v2Tag
|
||||||
|
saveFile(filePath)
|
||||||
|
}catch (e: java.io.FileNotFoundException){
|
||||||
|
try {
|
||||||
|
//Image Still Not Downloaded!
|
||||||
|
//Lets Download Now and Write it into Album Art
|
||||||
|
downloadFile(track.albumArtURL).collect {
|
||||||
|
when(it){
|
||||||
|
is DownloadResult.Error -> {}//Error
|
||||||
|
is DownloadResult.Success -> {
|
||||||
|
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||||
|
this.id3v2Tag = id3v2Tag
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e: Exception){
|
||||||
|
//log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Mp3File.saveFile(filePath: String){
|
||||||
|
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||||
|
val file = File(filePath)
|
||||||
|
file.delete()
|
||||||
|
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
|
||||||
|
newFile.renameTo(file)
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
|
||||||
|
actual data class Picture(
|
||||||
|
var source: String = "",
|
||||||
|
var name: String = "",
|
||||||
|
var image: Bitmap,
|
||||||
|
var width: Int = 0,
|
||||||
|
var height: Int = 0,
|
||||||
|
var id: Int = 0
|
||||||
|
)
|
@ -25,7 +25,6 @@ import com.shabinder.database.Database
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
|
|
||||||
actual class YoutubeProvider actual constructor(
|
actual class YoutubeProvider actual constructor(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
@ -108,7 +107,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = it.title(),
|
title = it.title(),
|
||||||
artists = listOf(it.author().toString()),
|
artists = listOf(it.author().toString()),
|
||||||
durationSec = it.lengthSeconds(),
|
durationSec = it.lengthSeconds(),
|
||||||
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
@ -178,7 +177,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail?.author().toString()),
|
artists = listOf(detail?.author().toString()),
|
||||||
durationSec = detail?.lengthSeconds() ?: 0,
|
durationSec = detail?.lengthSeconds() ?: 0,
|
||||||
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
albumArtPath = dir.imageCacheDir() + "$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 (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
|
@ -51,7 +51,7 @@ fun isInternetAvailable(): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
fun createHttpClient(enableNetworkLogs: Boolean = false,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
this.serializer = serializer
|
this.serializer = serializer
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.shabinder.common.utils.removeIllegalChars
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
expect open class Dir(
|
||||||
|
logger: Kermit,
|
||||||
|
) {
|
||||||
|
fun isPresent(path:String):Boolean
|
||||||
|
fun fileSeparator(): String
|
||||||
|
fun defaultDir(): String
|
||||||
|
fun imageCacheDir(): String
|
||||||
|
fun createDirectory(dirPath:String)
|
||||||
|
fun cacheImage(picture: Picture)
|
||||||
|
suspend fun clearCache()
|
||||||
|
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, path: String, trackDetails: TrackDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun Dir.downloadFile(url: String): Flow<DownloadResult> {
|
||||||
|
return flow {
|
||||||
|
val client = createHttpClient()
|
||||||
|
val response = client.get<HttpStatement>(url).execute()
|
||||||
|
val data = ByteArray(response.contentLength()!!.toInt())
|
||||||
|
var offset = 0
|
||||||
|
do {
|
||||||
|
val currentRead = response.content.readAvailable(data, offset, data.size)
|
||||||
|
offset += currentRead
|
||||||
|
val progress = (offset * 100f / data.size).roundToInt()
|
||||||
|
emit(DownloadResult.Progress(progress))
|
||||||
|
} while (currentRead > 0)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
emit(DownloadResult.Success(data))
|
||||||
|
} else {
|
||||||
|
emit(DownloadResult.Error("File not downloaded"))
|
||||||
|
}
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
||||||
|
return flow {
|
||||||
|
val client = createHttpClient()
|
||||||
|
val response = client.get<HttpStatement>(url).execute()
|
||||||
|
val data = ByteArray(response.contentLength()!!.toInt())
|
||||||
|
var offset = 0
|
||||||
|
do {
|
||||||
|
val currentRead = response.content.readAvailable(data, offset, data.size)
|
||||||
|
offset += currentRead
|
||||||
|
val progress = (offset * 100f / data.size).roundToInt()
|
||||||
|
emit(DownloadResult.Progress(progress))
|
||||||
|
} while (currentRead > 0)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
emit(DownloadResult.Success(data))
|
||||||
|
} else {
|
||||||
|
emit(DownloadResult.Error("File not downloaded"))
|
||||||
|
}
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dir.cacheImagePostfix():String = "info"
|
||||||
|
fun Dir.getNameURL(url: String): String {
|
||||||
|
return url.substring(url.lastIndexOf('/') + 1, url.length)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Call this function at startup!
|
||||||
|
* */
|
||||||
|
fun Dir.createDirectories() {
|
||||||
|
createDirectory(defaultDir())
|
||||||
|
createDirectory(imageCacheDir())
|
||||||
|
createDirectory(defaultDir() + "Tracks/")
|
||||||
|
createDirectory(defaultDir() + "Albums/")
|
||||||
|
createDirectory(defaultDir() + "Playlists/")
|
||||||
|
createDirectory(defaultDir() + "YT_Downloads/")
|
||||||
|
}
|
||||||
|
fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
||||||
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
|
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
||||||
|
removeIllegalChars(itemName) + extension
|
@ -1,35 +1,14 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.utils.removeIllegalChars
|
import com.shabinder.common.utils.removeIllegalChars
|
||||||
|
|
||||||
|
expect class Picture
|
||||||
|
|
||||||
expect fun openPlatform(platformID:String ,platformLink:String)
|
expect fun openPlatform(platformID:String ,platformLink:String)
|
||||||
|
|
||||||
expect fun shareApp()
|
expect fun shareApp()
|
||||||
|
|
||||||
expect fun giveDonation()
|
expect fun giveDonation()
|
||||||
|
|
||||||
expect fun downloadTracks(list: List<TrackDetails>)
|
expect fun downloadTracks(list: List<TrackDetails>)
|
||||||
|
|
||||||
expect open class Dir(
|
|
||||||
logger: Kermit
|
|
||||||
) {
|
|
||||||
fun isPresent(path:String):Boolean
|
|
||||||
fun fileSeparator(): String
|
|
||||||
fun defaultDir(): String
|
|
||||||
fun imageDir(): String
|
|
||||||
fun createDirectory(dirPath:String)
|
|
||||||
}
|
|
||||||
fun Dir.createDirectories() {
|
|
||||||
createDirectory(defaultDir())
|
|
||||||
createDirectory(imageDir())
|
|
||||||
createDirectory(defaultDir() + "Tracks/")
|
|
||||||
createDirectory(defaultDir() + "Albums/")
|
|
||||||
createDirectory(defaultDir() + "Playlists/")
|
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
|
||||||
}
|
|
||||||
fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
|
||||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
|
||||||
removeIllegalChars(itemName) + extension
|
|
@ -208,7 +208,7 @@ class GaanaProvider(
|
|||||||
title = it.track_title,
|
title = it.track_title,
|
||||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
durationSec = it.duration,
|
durationSec = it.duration,
|
||||||
albumArtPath = dir.imageDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||||
albumName = it.album_title,
|
albumName = it.album_title,
|
||||||
year = it.release_date,
|
year = it.release_date,
|
||||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||||
|
@ -231,7 +231,7 @@ class SpotifyProvider(
|
|||||||
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(),
|
||||||
durationSec = (it.duration_ms/1000).toInt(),
|
durationSec = (it.duration_ms/1000).toInt(),
|
||||||
albumArtPath = dir.imageDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
||||||
albumName = it.album?.name,
|
albumName = it.album?.name,
|
||||||
year = it.album?.release_date,
|
year = it.album?.release_date,
|
||||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import java.io.File
|
import java.io.*
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
actual fun openPlatform(platformID:String ,platformLink:String){
|
actual fun openPlatform(platformID:String ,platformLink:String){
|
||||||
//TODO
|
//TODO
|
||||||
@ -17,34 +19,4 @@ actual fun giveDonation(){
|
|||||||
|
|
||||||
actual fun downloadTracks(list: List<TrackDetails>){
|
actual fun downloadTracks(list: List<TrackDetails>){
|
||||||
//TODO
|
//TODO
|
||||||
}
|
|
||||||
|
|
||||||
actual open class Dir actual constructor(private val logger: Kermit) {
|
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
|
||||||
|
|
||||||
actual fun imageDir(): String = System.getProperty("user.home") + ".images" + File.separator
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() +
|
|
||||||
"SpotiFlyer" + fileSeparator()
|
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
|
||||||
|
|
||||||
actual fun createDirectory(dirPath:String){
|
|
||||||
val yourAppDir = File(dirPath)
|
|
||||||
|
|
||||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
|
||||||
{ // create empty directory
|
|
||||||
if (yourAppDir.mkdirs())
|
|
||||||
{logger.i{"$dirPath created"}}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.e{"Unable to create Dir: $dirPath!"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.i { "$dirPath already exists" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.mpatric.mp3agic.Mp3File
|
||||||
|
import java.io.*
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
actual open class Dir actual constructor(private val logger: Kermit) {
|
||||||
|
|
||||||
|
actual fun fileSeparator(): String = File.separator
|
||||||
|
|
||||||
|
actual fun imageCacheDir(): String = System.getProperty("user.home") +
|
||||||
|
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||||
|
|
||||||
|
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() +
|
||||||
|
"SpotiFlyer" + fileSeparator()
|
||||||
|
|
||||||
|
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||||
|
|
||||||
|
actual fun createDirectory(dirPath:String){
|
||||||
|
val yourAppDir = File(dirPath)
|
||||||
|
|
||||||
|
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
||||||
|
{ // create empty directory
|
||||||
|
if (yourAppDir.mkdirs())
|
||||||
|
{logger.i{"$dirPath created"}}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.e{"Unable to create Dir: $dirPath!"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.i { "$dirPath already exists" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun clearCache() {
|
||||||
|
File(imageCacheDir()).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun cacheImage(picture: Picture) {
|
||||||
|
try {
|
||||||
|
val path = imageCacheDir() + picture.name
|
||||||
|
|
||||||
|
ImageIO.write(picture.image, "jpeg", File(path))
|
||||||
|
|
||||||
|
val bw =
|
||||||
|
BufferedWriter(
|
||||||
|
OutputStreamWriter(
|
||||||
|
FileOutputStream(path + cacheImagePostfix()),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
bw.write(picture.source)
|
||||||
|
bw.write("\r\n${picture.width}")
|
||||||
|
bw.write("\r\n${picture.height}")
|
||||||
|
bw.close()
|
||||||
|
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
actual suspend fun saveFileWithMetadata(
|
||||||
|
mp3ByteArray: ByteArray,
|
||||||
|
path: String,
|
||||||
|
trackDetails: TrackDetails
|
||||||
|
) {
|
||||||
|
val file = File(path)
|
||||||
|
file.writeBytes(mp3ByteArray)
|
||||||
|
|
||||||
|
Mp3File(file)
|
||||||
|
.removeAllTags()
|
||||||
|
.setId3v1Tags(trackDetails)
|
||||||
|
.setId3v2TagsAndSaveFile(trackDetails,path)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import com.mpatric.mp3agic.ID3v1Tag
|
||||||
|
import com.mpatric.mp3agic.ID3v24Tag
|
||||||
|
import com.mpatric.mp3agic.Mp3File
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
fun Mp3File.removeAllTags(): Mp3File {
|
||||||
|
if (hasId3v1Tag()) removeId3v1Tag()
|
||||||
|
if (hasId3v2Tag()) removeId3v2Tag()
|
||||||
|
if (hasCustomTag()) removeCustomTag()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifying Mp3 with MetaData!
|
||||||
|
**/
|
||||||
|
fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
||||||
|
val id3v1Tag = ID3v1Tag().apply {
|
||||||
|
artist = track.artists.joinToString(",")
|
||||||
|
title = track.title
|
||||||
|
album = track.albumName
|
||||||
|
year = track.year
|
||||||
|
comment = "Genres:${track.comment}"
|
||||||
|
}
|
||||||
|
this.id3v1Tag = id3v1Tag
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails,filePath:String){
|
||||||
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
|
artist = track.artists.joinToString(",")
|
||||||
|
title = track.title
|
||||||
|
album = track.albumName
|
||||||
|
year = track.year
|
||||||
|
comment = "Genres:${track.comment}"
|
||||||
|
lyrics = "Gonna Implement Soon"
|
||||||
|
url = track.trackUrl
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
val art = File(track.albumArtPath)
|
||||||
|
val bytesArray = ByteArray(art.length().toInt())
|
||||||
|
val fis = FileInputStream(art)
|
||||||
|
fis.read(bytesArray) //read file into bytes[]
|
||||||
|
fis.close()
|
||||||
|
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||||
|
this.id3v2Tag = id3v2Tag
|
||||||
|
saveFile(filePath)
|
||||||
|
}catch (e: java.io.FileNotFoundException){
|
||||||
|
try {
|
||||||
|
//Image Still Not Downloaded!
|
||||||
|
//Lets Download Now and Write it into Album Art
|
||||||
|
downloadFile(track.albumArtURL).collect {
|
||||||
|
when(it){
|
||||||
|
is DownloadResult.Error -> {}//Error
|
||||||
|
is DownloadResult.Success -> {
|
||||||
|
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||||
|
this.id3v2Tag = id3v2Tag
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e: Exception){
|
||||||
|
//log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Mp3File.saveFile(filePath: String){
|
||||||
|
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||||
|
val file = File(filePath)
|
||||||
|
file.delete()
|
||||||
|
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
|
||||||
|
newFile.renameTo(file)
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
|
||||||
|
actual data class Picture(
|
||||||
|
var source: String = "",
|
||||||
|
var name: String = "",
|
||||||
|
var image: BufferedImage,
|
||||||
|
var width: Int = 0,
|
||||||
|
var height: Int = 0,
|
||||||
|
var id: Int = 0
|
||||||
|
)
|
@ -108,7 +108,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = it.title(),
|
title = it.title(),
|
||||||
artists = listOf(it.author().toString()),
|
artists = listOf(it.author().toString()),
|
||||||
durationSec = it.lengthSeconds(),
|
durationSec = it.lengthSeconds(),
|
||||||
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
@ -178,7 +178,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail?.author().toString()),
|
artists = listOf(detail?.author().toString()),
|
||||||
durationSec = detail?.lengthSeconds() ?: 0,
|
durationSec = detail?.lengthSeconds() ?: 0,
|
||||||
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
albumArtPath = dir.imageCacheDir() + "$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 (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
|
Loading…
Reference in New Issue
Block a user