Youtube Music Fetching Fixed for Native

This commit is contained in:
shabinder 2021-05-04 16:14:03 +05:30
parent 7a3a299aa2
commit c278d6379a
13 changed files with 163 additions and 46 deletions

View File

@ -264,6 +264,8 @@ class MainActivity : ComponentActivity(), PaymentResultListener {
} }
} }
override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
override val isInternetAvailable get() = internetAvailability.value ?: true override val isInternetAvailable get() = internetAvailability.value ?: true
} }
} }

View File

@ -38,6 +38,7 @@ interface Actions {
// Open / Redirect to another Platform // Open / Redirect to another Platform
fun openPlatform(packageID: String, platformLink: String) fun openPlatform(packageID: String, platformLink: String)
fun writeMp3Tags(trackDetails: TrackDetails)
} }
@ -49,5 +50,7 @@ private fun stubActions() = object :Actions{
override fun giveDonation() {} override fun giveDonation() {}
override fun shareApp() {} override fun shareApp() {}
override fun openPlatform(packageID: String, platformLink: String) {} override fun openPlatform(packageID: String, platformLink: String) {}
override fun writeMp3Tags(trackDetails: TrackDetails) {}
override val isInternetAvailable: Boolean = true override val isInternetAvailable: Boolean = true
} }

View File

@ -3,6 +3,7 @@ package com.shabinder.common.models
import kotlin.native.concurrent.AtomicReference import kotlin.native.concurrent.AtomicReference
actual interface PlatformActions {} actual interface PlatformActions {}
actual val StubPlatformActions = object: PlatformActions {} actual val StubPlatformActions = object: PlatformActions {}
actual typealias NativeAtomicReference<T> = AtomicReference<T> actual typealias NativeAtomicReference<T> = AtomicReference<T>

View File

@ -72,6 +72,9 @@ val kotlinxSerializer = KotlinxSerializer(
fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
// https://github.com/Kotlin/kotlinx.serialization/issues/1450 // https://github.com/Kotlin/kotlinx.serialization/issues/1450
install(JsonFeature) {
serializer = KotlinxSerializer()
}
/*install(JsonFeature) { /*install(JsonFeature) {
serializer = KotlinxSerializer( serializer = KotlinxSerializer(
Json { Json {

View File

@ -30,6 +30,7 @@ class YoutubeMp3(
private val dir: Dir, private val dir: Dir,
) : Yt1sMp3 { ) : Yt1sMp3 {
suspend fun getMp3DownloadLink(videoID: String): String? = try { suspend fun getMp3DownloadLink(videoID: String): String? = try {
logger.i { "Youtube MP3 Link Fetching!" }
getLinkFromYt1sMp3(videoID)?.let { getLinkFromYt1sMp3(videoID)?.let {
logger.i { "Download Link: $it" } logger.i { "Download Link: $it" }
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/) if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)

View File

@ -29,6 +29,7 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType import io.ktor.http.contentType
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
@ -65,7 +66,7 @@ class YoutubeMusic constructor(
val youtubeTracks = mutableListOf<YoutubeTrack>() val youtubeTracks = mutableListOf<YoutubeTrack>()
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query)) val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
logger.i { "Youtube Music Response Recieved" }
val contentBlocks = responseObj.jsonObject["contents"] val contentBlocks = responseObj.jsonObject["contents"]
?.jsonObject?.get("sectionListRenderer") ?.jsonObject?.get("sectionListRenderer")
?.jsonObject?.get("contents")?.jsonArray ?.jsonObject?.get("contents")?.jsonArray
@ -284,7 +285,8 @@ class YoutubeMusic constructor(
} }
private suspend fun getYoutubeMusicResponse(query: String): String { 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) contentType(ContentType.Application.Json)
headers { headers {
append("referer", "https://music.youtube.com/search") append("referer", "https://music.youtube.com/search")

View File

@ -49,6 +49,7 @@ suspend inline fun <reified T: Any> HttpClient.postData(
): T { ): T {
val response = post<HttpResponse> { val response = post<HttpResponse> {
url.takeFrom(urlString) url.takeFrom(urlString)
header(HttpHeaders.ContentType, ContentType.Application.Json)
block() block()
} }
val jsonBody = response.readText() val jsonBody = response.readText()

View File

@ -16,6 +16,7 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadResult
@ -46,7 +47,7 @@ actual suspend fun downloadTracks(
list.forEach { list.forEach {
DownloadScope.execute { // Send Download to Pool. DownloadScope.execute { // Send Download to Pool.
if (!it.videoID.isNullOrBlank()) { // Video ID already known! if (!it.videoID.isNullOrBlank()) { // Video ID already known!
downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata) downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata, fetcher.youtubeMp3)
} else { } else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}" val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it) val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
@ -57,7 +58,7 @@ actual suspend fun downloadTracks(
) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) } ) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) }
) )
} else { // Found Youtube Video ID } 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( suspend fun downloadTrack(
videoID: String, videoID: String,
trackDetails: TrackDetails, 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 { try {
val audioData = ytDownloader.getVideo(videoID).getData() var link = youtubeMp3.getMp3DownloadLink(videoID)
audioData?.let { format -> if (link == null) {
val url = format.url ?: return link = ytDownloader.getVideo(videoID).getData()?.url ?: return
downloadFile(url).collect { }
when (it) { downloadFile(link).collect {
is DownloadResult.Error -> { when (it) {
DownloadProgressFlow.emit( is DownloadResult.Error -> {
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.emit(
0 DownloadProgressFlow.replayCache.getOrElse(
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) } 0
) ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
} )
is DownloadResult.Progress -> { }
DownloadProgressFlow.emit( is DownloadResult.Progress -> {
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.emit(
0 DownloadProgressFlow.replayCache.getOrElse(
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) } 0
) ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
} )
is DownloadResult.Success -> { // Todo clear map }
saveFileWithMetaData(it.byteArray, trackDetails){} is DownloadResult.Success -> { // Todo clear map
DownloadProgressFlow.emit( saveFileWithMetaData(it.byteArray, trackDetails){}
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.emit(
0 DownloadProgressFlow.replayCache.getOrElse(
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) } 0
) ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
} )
} }
} }
} }

View File

@ -1,8 +1,15 @@
package com.shabinder.common.di 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.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.methods
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collect
@SharedImmutable @SharedImmutable
actual val dispatcherIO = Dispatchers.Default actual val dispatcherIO = Dispatchers.Default
@ -10,10 +17,85 @@ actual val dispatcherIO = Dispatchers.Default
@SharedImmutable @SharedImmutable
actual val currentPlatform: AllPlatforms = AllPlatforms.Native actual val currentPlatform: AllPlatforms = AllPlatforms.Native
@SharedImmutable
val Downloader = ParallelExecutor(dispatcherIO)
actual suspend fun downloadTracks( actual suspend fun downloadTracks(
list: List<TrackDetails>, list: List<TrackDetails>,
fetcher: FetchPlatformQueryResult, fetcher: FetchPlatformQueryResult,
dir: Dir dir: Dir
) { ) {
// TODO 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<HashMap<String, DownloadStatus>> = 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()
}
} }

View File

@ -12,6 +12,6 @@ object IOSDeps: KoinComponent {
val dir: Dir by inject() // = get() val dir: Dir by inject() // = get()
val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get() val fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get()
val database get() = dir.db val database get() = dir.db
val sharedFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1) val sharedFlow = DownloadProgressFlow
val defaultDispatcher = dispatcherDefault val defaultDispatcher = dispatcherDefault
} }

View File

@ -22,7 +22,7 @@ import platform.UIKit.UIImage
import platform.UIKit.UIImageJPEGRepresentation import platform.UIKit.UIImageJPEGRepresentation
actual class Dir actual constructor( actual class Dir actual constructor(
private val logger: Kermit, val logger: Kermit,
private val spotiFlyerDatabase: SpotiFlyerDatabase, private val spotiFlyerDatabase: SpotiFlyerDatabase,
) { ) {
@ -134,16 +134,36 @@ actual class Dir actual constructor(
trackDetails: TrackDetails, trackDetails: TrackDetails,
postProcess:(track: TrackDetails)->Unit postProcess:(track: TrackDetails)->Unit
) : Unit = withContext(dispatcherIO) { ) : Unit = withContext(dispatcherIO) {
when (trackDetails.outputFilePath.substringAfterLast('.')) { try {
".mp3" -> { if (mp3ByteArray.isNotEmpty()) {
postProcess(trackDetails) mp3ByteArray.toNSData().writeToFile(
/*val file = TLAudio(trackDetails.outputFilePath) trackDetails.outputFilePath,
file.addTagsAndSave( 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, trackDetails,
this::loadCachedImage, this::loadCachedImage,
this::addToLibrary this::addToLibrary
)*/ )*/
}
} }
}catch (e:Exception){
e.printStackTrace()
} }
} }

View File

@ -33,6 +33,7 @@ import com.shabinder.common.di.isInternetAccessible
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.uikit.SpotiFlyerColors import com.shabinder.common.uikit.SpotiFlyerColors
import com.shabinder.common.uikit.SpotiFlyerRootContent import com.shabinder.common.uikit.SpotiFlyerRootContent
@ -92,14 +93,12 @@ private fun spotiFlyerRoot(componentContext: ComponentContext): SpotiFlyerRoot =
override fun openPlatform(packageID: String, platformLink: String) {} override fun openPlatform(packageID: String, platformLink: String) {}
override val dispatcherIO = Dispatchers.IO override fun writeMp3Tags(trackDetails: TrackDetails) {/*IMPLEMENTED*/}
override val isInternetAvailable: Boolean override val isInternetAvailable: Boolean
get() = runBlocking { get() = runBlocking {
isInternetAccessible() isInternetAccessible()
} }
override val currentPlatform = AllPlatforms.Jvm
} }
} }
) )

View File

@ -25,6 +25,7 @@ import com.shabinder.common.di.DownloadProgressFlow
import com.shabinder.common.models.Actions import com.shabinder.common.models.Actions
import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.database.Database import com.shabinder.database.Database
import extras.renderableChild import extras.renderableChild
@ -77,9 +78,9 @@ class App(props: AppProps): RComponent<AppProps, RState>(props) {
override fun openPlatform(packageID: String, platformLink: String) {} 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 isInternetAvailable: Boolean = true
override val currentPlatform = AllPlatforms.Js
} }
} }
) )