diff --git a/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt index a4c62da4..4d3755ff 100644 --- a/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt +++ b/common/compose-ui/src/androidMain/kotlin/com/shabinder/common/ui/Actual.kt @@ -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, + 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() +} \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt index d277e1ff..2ff87db1 100644 --- a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Expect.kt @@ -11,4 +11,6 @@ expect fun ImageLoad( loadingResource: ImageBitmap? = null, errorResource: ImageBitmap? = null, modifier: Modifier = Modifier -) \ No newline at end of file +) + +expect fun showPopUpMessage(text: String) \ No newline at end of file diff --git a/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Toast.kt b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Toast.kt new file mode 100644 index 00000000..e442e80f --- /dev/null +++ b/common/compose-ui/src/commonMain/kotlin/com/shabinder/common/ui/Toast.kt @@ -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 = mutableStateOf(false), + duration: ToastDuration = ToastDuration.Long +) \ No newline at end of file diff --git a/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Toast.kt b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Toast.kt new file mode 100644 index 00000000..a99cb435 --- /dev/null +++ b/common/compose-ui/src/desktopMain/kotlin/com/shabinder/common/ui/Toast.kt @@ -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 = mutableStateOf("") +private val state: MutableState = 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, + 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 { } + } + } + } + } +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadResult.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadResult.kt new file mode 100644 index 00000000..6a8f72d9 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/DownloadResult.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index d7664760..e9e55d14 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -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 { diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt index ead69ddd..d51d85e8 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Actual.kt @@ -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 @@ -20,24 +22,4 @@ actual fun giveDonation(){ actual fun downloadTracks(list: List){ //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) { - } } \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt new file mode 100644 index 00000000..15ac243d --- /dev/null +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Dir.kt @@ -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) + } +} \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/ID3Tagging.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/ID3Tagging.kt new file mode 100644 index 00000000..bd5f7643 --- /dev/null +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/ID3Tagging.kt @@ -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) +} diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Picture.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Picture.kt new file mode 100644 index 00000000..7bfe7bc7 --- /dev/null +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/Picture.kt @@ -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 +) \ No newline at end of file diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt index e4eb0f11..7ce7bba7 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/YoutubeProvider.kt @@ -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( diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt index 2f8bfa16..b88993be 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/DI.kt @@ -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 } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt new file mode 100644 index 00000000..63bccc6f --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Dir.kt @@ -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 { + return flow { + val client = createHttpClient() + val response = client.get(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 { + return flow { + val client = createHttpClient() + val response = client.get(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 diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt index 89defefb..a2a5a616 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/Expect.kt @@ -1,35 +1,14 @@ 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() expect fun giveDonation() -expect fun downloadTracks(list: List) - -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 +expect fun downloadTracks(list: List) \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt index f7677dab..27045ad5 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/GaanaProvider.kt @@ -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 }}", diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt index 246c368e..fe147dc3 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/providers/SpotifyProvider.kt @@ -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()}", diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt index 84ca723c..3843d090 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Actual.kt @@ -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 @@ -17,34 +19,4 @@ actual fun giveDonation(){ actual fun downloadTracks(list: List){ //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" } - } - } } \ No newline at end of file diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt new file mode 100644 index 00000000..74837340 --- /dev/null +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Dir.kt @@ -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) + } +} diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/ID3Tagging.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/ID3Tagging.kt new file mode 100644 index 00000000..3699587d --- /dev/null +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/ID3Tagging.kt @@ -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) +} diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Picture.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Picture.kt new file mode 100644 index 00000000..d34aa1cf --- /dev/null +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/Picture.kt @@ -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 +) \ No newline at end of file diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt index 25c9f740..9ca528c7 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/YoutubeProvider.kt @@ -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(