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
}
}

View File

@ -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
}

View File

@ -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<T> = AtomicReference<T>

View File

@ -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 {

View File

@ -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*/)

View File

@ -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<YoutubeTrack>()
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")

View File

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

View File

@ -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) }
)
}
}
}

View File

@ -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<TrackDetails>,
fetcher: FetchPlatformQueryResult,
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 fetchPlatformQueryResult: FetchPlatformQueryResult by inject() // get()
val database get() = dir.db
val sharedFlow = MutableSharedFlow<HashMap<String, DownloadStatus>>(1)
val sharedFlow = DownloadProgressFlow
val defaultDispatcher = dispatcherDefault
}

View File

@ -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()
}
}

View File

@ -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
}
}
)

View File

@ -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<AppProps, RState>(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
}
}
)