diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 8403029c..a083d5f5 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -264,6 +264,8 @@ class MainActivity : ComponentActivity(), PaymentResultListener { } } + override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/} + override val isInternetAvailable get() = internetAvailability.value ?: true } } diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/Actions.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/Actions.kt index 68b8a516..9d3c9dde 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/Actions.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/Actions.kt @@ -38,6 +38,7 @@ interface Actions { // Open / Redirect to another Platform fun openPlatform(packageID: String, platformLink: String) + fun writeMp3Tags(trackDetails: TrackDetails) } @@ -49,5 +50,7 @@ private fun stubActions() = object :Actions{ override fun giveDonation() {} override fun shareApp() {} override fun openPlatform(packageID: String, platformLink: String) {} + override fun writeMp3Tags(trackDetails: TrackDetails) {} + override val isInternetAvailable: Boolean = true } \ No newline at end of file diff --git a/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSPlatformActions.kt b/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSPlatformActions.kt index 3b621244..14a05e4b 100644 --- a/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSPlatformActions.kt +++ b/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSPlatformActions.kt @@ -3,6 +3,7 @@ package com.shabinder.common.models import kotlin.native.concurrent.AtomicReference actual interface PlatformActions {} + actual val StubPlatformActions = object: PlatformActions {} actual typealias NativeAtomicReference = AtomicReference \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index 5e61d3ed..74ff0b76 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -72,6 +72,9 @@ val kotlinxSerializer = KotlinxSerializer( fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { // https://github.com/Kotlin/kotlinx.serialization/issues/1450 + install(JsonFeature) { + serializer = KotlinxSerializer() + } /*install(JsonFeature) { serializer = KotlinxSerializer( Json { diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt index 84d46e30..70ba6296 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMp3.kt @@ -30,6 +30,7 @@ class YoutubeMp3( private val dir: Dir, ) : Yt1sMp3 { suspend fun getMp3DownloadLink(videoID: String): String? = try { + logger.i { "Youtube MP3 Link Fetching!" } getLinkFromYt1sMp3(videoID)?.let { logger.i { "Download Link: $it" } if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt index 696ad3a8..8c859b8a 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt @@ -29,6 +29,7 @@ import io.ktor.http.ContentType import io.ktor.http.contentType import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.contentOrNull @@ -65,7 +66,7 @@ class YoutubeMusic constructor( val youtubeTracks = mutableListOf() val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query)) - + logger.i { "Youtube Music Response Recieved" } val contentBlocks = responseObj.jsonObject["contents"] ?.jsonObject?.get("sectionListRenderer") ?.jsonObject?.get("contents")?.jsonArray @@ -284,7 +285,8 @@ class YoutubeMusic constructor( } private suspend fun getYoutubeMusicResponse(query: String): String { - return httpClient.postData("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") { + logger.i { "Fetching Youtube Music Response" } + return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") { contentType(ContentType.Application.Json) headers { append("referer", "https://music.youtube.com/search") diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt index 88fcc775..18ba7fea 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt @@ -49,6 +49,7 @@ suspend inline fun HttpClient.postData( ): T { val response = post { url.takeFrom(urlString) + header(HttpHeaders.ContentType, ContentType.Application.Json) block() } val jsonBody = response.readText() diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index 3ab77fab..683b6459 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -16,6 +16,7 @@ package com.shabinder.common.di +import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.DownloadResult @@ -46,7 +47,7 @@ actual suspend fun downloadTracks( list.forEach { DownloadScope.execute { // Send Download to Pool. if (!it.videoID.isNullOrBlank()) { // Video ID already known! - downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata) + downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata, fetcher.youtubeMp3) } else { val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it) @@ -57,7 +58,7 @@ actual suspend fun downloadTracks( ) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) } ) } else { // Found Youtube Video ID - downloadTrack(videoId, it, dir::saveFileWithMetadata) + downloadTrack(videoId, it, dir::saveFileWithMetadata,fetcher.youtubeMp3) } } } @@ -69,37 +70,38 @@ private val ytDownloader = YoutubeDownloader() suspend fun downloadTrack( videoID: String, trackDetails: TrackDetails, - saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(TrackDetails)->Unit) -> Unit + saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (TrackDetails) -> Unit) -> Unit, + youtubeMp3: YoutubeMp3 ) { try { - val audioData = ytDownloader.getVideo(videoID).getData() + var link = youtubeMp3.getMp3DownloadLink(videoID) - audioData?.let { format -> - val url = format.url ?: return - downloadFile(url).collect { - when (it) { - is DownloadResult.Error -> { - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } - ) - } - is DownloadResult.Progress -> { - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) } - ) - } - is DownloadResult.Success -> { // Todo clear map - saveFileWithMetaData(it.byteArray, trackDetails){} - DownloadProgressFlow.emit( - DownloadProgressFlow.replayCache.getOrElse( - 0 - ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } - ) - } + if (link == null) { + link = ytDownloader.getVideo(videoID).getData()?.url ?: return + } + downloadFile(link).collect { + when (it) { + is DownloadResult.Error -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } + ) + } + is DownloadResult.Progress -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) } + ) + } + is DownloadResult.Success -> { // Todo clear map + saveFileWithMetaData(it.byteArray, trackDetails){} + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } + ) } } } diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt index 39782f98..4030dc19 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt @@ -1,8 +1,15 @@ package com.shabinder.common.di +import com.shabinder.common.di.providers.YoutubeMp3 +import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.AllPlatforms +import com.shabinder.common.models.DownloadResult +import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.methods import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.collect @SharedImmutable actual val dispatcherIO = Dispatchers.Default @@ -10,10 +17,85 @@ actual val dispatcherIO = Dispatchers.Default @SharedImmutable actual val currentPlatform: AllPlatforms = AllPlatforms.Native +@SharedImmutable +val Downloader = ParallelExecutor(dispatcherIO) + actual suspend fun downloadTracks( list: List, fetcher: FetchPlatformQueryResult, dir: Dir ) { - // TODO -} \ No newline at end of file + dir.logger.i { "Downloading ${list.size} Tracks" } + for (track in list) { + Downloader.execute { + if (!track.videoID.isNullOrBlank()) { // Video ID already known! + dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" } + downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata,fetcher) + } else { + val searchQuery = "${track.title} - ${track.artists.joinToString(",")}" + val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, track) + dir.logger.i { "VideoID: ${track.title} -> $videoId" } + if (videoId.isNullOrBlank()) { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) } + ) + } else { // Found Youtube Video ID + downloadTrack(videoId, track, dir::saveFileWithMetadata,fetcher) + } + } + } + } +} + +@SharedImmutable +val DownloadProgressFlow: MutableSharedFlow> = MutableSharedFlow(1) + +suspend fun downloadTrack( + videoID: String, + trackDetails: TrackDetails, + saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (TrackDetails) -> Unit) -> Unit, + fetcher: FetchPlatformQueryResult +) { + try { + var link = fetcher.youtubeMp3.getMp3DownloadLink(videoID) + + fetcher.dir.logger.i { "LINK: $videoID -> $link" } + if (link == null) { + link = fetcher.youtubeProvider.ytDownloader.getVideo(videoID).getData()?.url ?: return + } + fetcher.dir.logger.i { "LINK: $videoID -> $link" } + downloadFile(link).collect { + fetcher.dir.logger.d { it.toString() } + when (it) { + is DownloadResult.Error -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } + ) + } + is DownloadResult.Progress -> { + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { + set(trackDetails.title, DownloadStatus.Downloading(it.progress)) + } + ) + } + is DownloadResult.Success -> { // Todo clear map + saveFileWithMetaData(it.byteArray, trackDetails, methods.value::writeMp3Tags) + DownloadProgressFlow.emit( + DownloadProgressFlow.replayCache.getOrElse( + 0 + ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } + ) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } +} diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDeps.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDeps.kt index 59856fdd..8b6c1aca 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDeps.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDeps.kt @@ -12,6 +12,6 @@ object IOSDeps: KoinComponent { val dir: Dir by inject() // = get() val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get() val database get() = dir.db - val sharedFlow = MutableSharedFlow>(1) + val sharedFlow = DownloadProgressFlow val defaultDispatcher = dispatcherDefault } \ No newline at end of file diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt index 3a2ea657..4ef880b7 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt @@ -22,7 +22,7 @@ import platform.UIKit.UIImage import platform.UIKit.UIImageJPEGRepresentation actual class Dir actual constructor( - private val logger: Kermit, + val logger: Kermit, private val spotiFlyerDatabase: SpotiFlyerDatabase, ) { @@ -134,16 +134,36 @@ actual class Dir actual constructor( trackDetails: TrackDetails, postProcess:(track: TrackDetails)->Unit ) : Unit = withContext(dispatcherIO) { - when (trackDetails.outputFilePath.substringAfterLast('.')) { - ".mp3" -> { - postProcess(trackDetails) - /*val file = TLAudio(trackDetails.outputFilePath) - file.addTagsAndSave( + try { + if (mp3ByteArray.isNotEmpty()) { + mp3ByteArray.toNSData().writeToFile( + trackDetails.outputFilePath, + true + ) + } + when (trackDetails.outputFilePath.substringAfterLast('.')) { + ".mp3" -> { + if(!isPresent(trackDetails.albumArtPath)) { + val imageData = downloadByteArray( + trackDetails.albumArtURL + )?.toNSData() + if (imageData != null) { + UIImage.imageWithData(imageData)?.also { + cacheImage(it, trackDetails.albumArtPath) + } + } + } + postProcess(trackDetails) + /*val file = TLAudio(trackDetails.outputFilePath) + file.addTagsAndSave( trackDetails, this::loadCachedImage, this::addToLibrary )*/ + } } + }catch (e:Exception){ + e.printStackTrace() } } diff --git a/desktop/src/jvmMain/kotlin/Main.kt b/desktop/src/jvmMain/kotlin/Main.kt index a8e2dc64..039049f8 100644 --- a/desktop/src/jvmMain/kotlin/Main.kt +++ b/desktop/src/jvmMain/kotlin/Main.kt @@ -33,6 +33,7 @@ import com.shabinder.common.di.isInternetAccessible import com.shabinder.common.models.Actions import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.PlatformActions +import com.shabinder.common.models.TrackDetails import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.uikit.SpotiFlyerColors import com.shabinder.common.uikit.SpotiFlyerRootContent @@ -92,14 +93,12 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot = override fun openPlatform(packageID: String, platformLink: String) {} - override val dispatcherIO = Dispatchers.IO + override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/} override val isInternetAvailable: Boolean get() = runBlocking { isInternetAccessible() } - - override val currentPlatform = AllPlatforms.Jvm } } ) diff --git a/web-app/src/main/kotlin/App.kt b/web-app/src/main/kotlin/App.kt index 46309d69..e5978b97 100644 --- a/web-app/src/main/kotlin/App.kt +++ b/web-app/src/main/kotlin/App.kt @@ -25,6 +25,7 @@ import com.shabinder.common.di.DownloadProgressFlow import com.shabinder.common.models.Actions import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.PlatformActions +import com.shabinder.common.models.TrackDetails import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.database.Database import extras.renderableChild @@ -77,9 +78,9 @@ class App(props: AppProps): RComponent(props) { override fun openPlatform(packageID: String, platformLink: String) {} - override val dispatcherIO = Dispatchers.Default + override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/} + override val isInternetAvailable: Boolean = true - override val currentPlatform = AllPlatforms.Js } } )