mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-24 18:04:33 +01:00
Youtube Music Fetching Fixed for Native
This commit is contained in:
parent
7a3a299aa2
commit
c278d6379a
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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>
|
@ -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 {
|
||||||
|
@ -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*/)
|
||||||
|
@ -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")
|
||||||
|
@ -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()
|
||||||
|
@ -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,14 +70,16 @@ 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 {
|
}
|
||||||
|
downloadFile(link).collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
DownloadProgressFlow.emit(
|
DownloadProgressFlow.emit(
|
||||||
@ -102,7 +105,6 @@ suspend fun downloadTrack(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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,8 +134,25 @@ actual class Dir actual constructor(
|
|||||||
trackDetails: TrackDetails,
|
trackDetails: TrackDetails,
|
||||||
postProcess:(track: TrackDetails)->Unit
|
postProcess:(track: TrackDetails)->Unit
|
||||||
) : Unit = withContext(dispatcherIO) {
|
) : Unit = withContext(dispatcherIO) {
|
||||||
|
try {
|
||||||
|
if (mp3ByteArray.isNotEmpty()) {
|
||||||
|
mp3ByteArray.toNSData().writeToFile(
|
||||||
|
trackDetails.outputFilePath,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||||
".mp3" -> {
|
".mp3" -> {
|
||||||
|
if(!isPresent(trackDetails.albumArtPath)) {
|
||||||
|
val imageData = downloadByteArray(
|
||||||
|
trackDetails.albumArtURL
|
||||||
|
)?.toNSData()
|
||||||
|
if (imageData != null) {
|
||||||
|
UIImage.imageWithData(imageData)?.also {
|
||||||
|
cacheImage(it, trackDetails.albumArtPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
postProcess(trackDetails)
|
postProcess(trackDetails)
|
||||||
/*val file = TLAudio(trackDetails.outputFilePath)
|
/*val file = TLAudio(trackDetails.outputFilePath)
|
||||||
file.addTagsAndSave(
|
file.addTagsAndSave(
|
||||||
@ -145,6 +162,9 @@ actual class Dir actual constructor(
|
|||||||
)*/
|
)*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}catch (e:Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(path: String) {
|
actual fun addToLibrary(path: String) {
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user