mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 18:24:31 +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.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.core.net.toUri
|
||||
import com.shabinder.common.database.appContext
|
||||
import dev.chrisbanes.accompanist.coil.CoilImage
|
||||
|
||||
@Composable
|
||||
@ -29,3 +31,16 @@ actual fun ImageLoad(
|
||||
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()
|
||||
}
|
@ -12,3 +12,5 @@ expect fun ImageLoad(
|
||||
errorResource: ImageBitmap? = null,
|
||||
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(Extras.kermit)
|
||||
api(Extras.jsonKlaxon)
|
||||
api(Extras.youtubeDownloader)
|
||||
//api(Extras.fuzzyWuzzy)
|
||||
//api("com.github.willowtreeapps:fuzzywuzzy-kotlin:v0.1.1")
|
||||
api(Extras.mp3agic)
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
|
@ -1,10 +1,12 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Environment
|
||||
import co.touchlab.kermit.Kermit
|
||||
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){
|
||||
//TODO
|
||||
@ -21,23 +23,3 @@ actual fun giveDonation(){
|
||||
actual fun downloadTracks(list: List<TrackDetails>){
|
||||
//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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.KoinComponent
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
private val httpClient: HttpClient,
|
||||
@ -108,7 +107,7 @@ actual class YoutubeProvider actual constructor(
|
||||
title = it.title(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
||||
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
@ -178,7 +177,7 @@ actual class YoutubeProvider actual constructor(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
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) {
|
||||
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,9 +1,10 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.utils.removeIllegalChars
|
||||
|
||||
expect class Picture
|
||||
|
||||
expect fun openPlatform(platformID:String ,platformLink:String)
|
||||
|
||||
expect fun shareApp()
|
||||
@ -11,25 +12,3 @@ expect fun shareApp()
|
||||
expect fun giveDonation()
|
||||
|
||||
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,
|
||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||
durationSec = it.duration,
|
||||
albumArtPath = dir.imageDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||
albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||
albumName = it.album_title,
|
||||
year = it.release_date,
|
||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||
|
@ -231,7 +231,7 @@ class SpotifyProvider(
|
||||
title = it.name.toString(),
|
||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||
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,
|
||||
year = it.album?.release_date,
|
||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||
|
@ -1,7 +1,9 @@
|
||||
package com.shabinder.common
|
||||
|
||||
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){
|
||||
//TODO
|
||||
@ -18,33 +20,3 @@ actual fun giveDonation(){
|
||||
actual fun downloadTracks(list: List<TrackDetails>){
|
||||
//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(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
||||
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
@ -178,7 +178,7 @@ actual class YoutubeProvider actual constructor(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
|
Loading…
Reference in New Issue
Block a user