Yt Api for Mp3 , Loading Anim

This commit is contained in:
shabinder 2021-02-24 19:46:16 +05:30
parent 189441111b
commit 760ffae8f3
19 changed files with 167 additions and 72 deletions

View File

@ -60,6 +60,10 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
/*lifecycleScope.launch {
val string = fetcher.youtubeMp3.getMp3DownloadLink("lVfVrqu1G0U")
Log.i("Mp3Test",string)
}*/
initialise() initialise()
} }

View File

@ -117,7 +117,7 @@ object Ktor {
} }
object Extras { object Extras {
const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.0" const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.1"
const val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1" const val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1"
const val mp3agic = "com.mpatric:mp3agic:0.9.1" const val mp3agic = "com.mpatric:mp3agic:0.9.1"
const val kermit = "co.touchlab:kermit:${Versions.kermit}" const val kermit = "co.touchlab:kermit:${Versions.kermit}"

View File

@ -58,10 +58,7 @@ interface SpotiFlyerList {
object Finished : Output() object Finished : Output()
} }
data class State( data class State(
val queryResult: PlatformQueryResult? = PlatformQueryResult( val queryResult: PlatformQueryResult? = null,
"","",
"Loading","", emptyList(),
Source.Spotify),
val link:String = "", val link:String = "",
val trackList:List<TrackDetails> = emptyList() val trackList:List<TrackDetails> = emptyList()
) )

View File

@ -21,11 +21,6 @@ import com.shabinder.common.ui.*
import com.shabinder.common.ui.SpotiFlyerTypography import com.shabinder.common.ui.SpotiFlyerTypography
import com.shabinder.common.ui.colorAccent import com.shabinder.common.ui.colorAccent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun SpotiFlyerListContent( fun SpotiFlyerListContent(
@ -38,8 +33,14 @@ fun SpotiFlyerListContent(
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
//TODO Better Null Handling //TODO Better Null Handling
val result = model.queryResult!! val result = model.queryResult
if(result == null){
Column(Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator()
Spacer(modifier.padding(8.dp))
Text("Loading..",style = appNameStyle,color = colorPrimary)
}
}else{
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
content = { content = {
@ -57,11 +58,12 @@ fun SpotiFlyerListContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
DownloadAllButton( DownloadAllButton(
onClick = {component.onDownloadAllClicked(result.trackList)}, onClick = {component.onDownloadAllClicked(model.trackList)},
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter) modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
) )
} }
} }
}
@Composable @Composable
fun TrackCard( fun TrackCard(

View File

@ -103,9 +103,9 @@ internal class SpotiFlyerListStoreProvider(
for(newTrack in map){ for(newTrack in map){
titleList.indexOf(newTrack.key).let { position -> titleList.indexOf(newTrack.key).let { position ->
if(position != -1){ if(position != -1){
updatedList.getOrNull(position)?.copy(downloaded = newTrack.value)?.also { updatedTrack -> updatedList.getOrNull(position)?.copy(downloaded = newTrack.value,progress = (newTrack.value as? DownloadStatus.Downloading)?.progress ?: updatedList[position].progress )?.also { updatedTrack ->
updatedList[position] = updatedTrack updatedList[position] = updatedTrack
logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update") //logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update")
} }
} }
} }

View File

@ -298,7 +298,7 @@ fun HistoryColumn(
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
content = { content = {
items(list) { items(list.distinctBy { it.coverUrl }) {
DownloadRecordItem( DownloadRecordItem(
item = it, item = it,
loadImage, loadImage,

View File

@ -35,6 +35,7 @@ data class TrackDetails(
var albumArtPath: String, var albumArtPath: String,
var albumArtURL: String, var albumArtURL: String,
var source: Source, var source: Source,
val progress: Int = 2,
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded, val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var outputFilePath: String, var outputFilePath: String,
var videoID:String? = null, var videoID:String? = null,

View File

@ -34,7 +34,7 @@ kotlin {
implementation(Ktor.clientAndroid) implementation(Ktor.clientAndroid)
implementation(Extras.Android.fetch) implementation(Extras.Android.fetch)
implementation(Koin.android) implementation(Koin.android)
api(files("$rootDir/libs/mobile-ffmpeg.aar")) //api(files("$rootDir/libs/mobile-ffmpeg.aar"))
} }
} }
desktopMain { desktopMain {

View File

@ -8,8 +8,6 @@ import android.os.Environment
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.FFmpeg
import com.mpatric.mp3agic.Mp3File import com.mpatric.mp3agic.Mp3File
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.database.appContext import com.shabinder.common.database.appContext
@ -78,13 +76,22 @@ actual class Dir actual constructor(
mp3ByteArray: ByteArray, mp3ByteArray: ByteArray,
trackDetails: TrackDetails trackDetails: TrackDetails
) { ) {
val m4aFile = File(trackDetails.outputFilePath) val songFile = File(trackDetails.outputFilePath)
/* /*
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received * Check , if Fetch was Used, File is saved Already, else write byteArray we Received
* */ * */
if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray) //if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
FFmpeg.executeAsync( when(trackDetails.outputFilePath.substringAfterLast('.')){
".mp3" -> {
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
}
".m4a" -> {
/*FFmpeg.executeAsync(
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}" "-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
){ _, returnCode -> ){ _, returnCode ->
when (returnCode) { when (returnCode) {
@ -106,6 +113,17 @@ actual class Dir actual constructor(
logger.d { "Async command execution failed with rc=$returnCode" } logger.d { "Async command execution failed with rc=$returnCode" }
} }
} }
}*/
}
else -> {
try{
Mp3File(File(songFile.absolutePath))
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(songFile.absolutePath)
}catch (e:Exception){e.printStackTrace()}
}
} }
} }

View File

@ -125,7 +125,7 @@ actual class YoutubeProvider actual constructor(
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
}, },
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"), outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
videoID = it.videoId() videoID = it.videoId()
) )
} }
@ -195,7 +195,7 @@ actual class YoutubeProvider actual constructor(
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
}, },
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"), outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
videoID = searchId videoID = searchId
) )
) )

View File

@ -178,11 +178,17 @@ class ForegroundService : Service(),CoroutineScope{
private fun downloadTrack(videoID:String, track: TrackDetails){ private fun downloadTrack(videoID:String, track: TrackDetails){
launch { launch {
try { try {
val audioData = ytDownloader.getVideo(videoID).getData() /*val audioData = ytDownloader.getVideo(videoID).getData()
audioData?.let { audioData?.let {
val url: String = it.url() val url: String = it.url()
logger.d("DHelper Link Found") { url } logger.d("DHelper Link Found") { url }
}*/
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if (url == null){
sendTrackBroadcast(Status.FAILED.name,track)
allTracksStatus[track.title] = DownloadStatus.Failed
} else{
val request= Request(url, track.outputFilePath).apply{ val request= Request(url, track.outputFilePath).apply{
priority = Priority.NORMAL priority = Priority.NORMAL
networkType = NetworkType.ALL networkType = NetworkType.ALL
@ -260,6 +266,7 @@ class ForegroundService : Service(),CoroutineScope{
addToNotification("Processing ${it.title}") addToNotification("Processing ${it.title}")
job.invokeOnCompletion { _ -> job.invokeOnCompletion { _ ->
converted++ converted++
allTracksStatus[it.title] = DownloadStatus.Downloaded
sendTrackBroadcast(Status.COMPLETED.name,it) sendTrackBroadcast(Status.COMPLETED.name,it)
removeFromNotification("Processing ${it.title}") removeFromNotification("Processing ${it.title}")
} }

View File

@ -5,6 +5,7 @@ import com.shabinder.common.database.createDatabase
import com.shabinder.common.database.getLogger import com.shabinder.common.database.getLogger
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeMusic
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.features.json.* import io.ktor.client.features.json.*
@ -33,7 +34,8 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { SpotifyProvider(get(),get(),get(),get()) } single { SpotifyProvider(get(),get(),get(),get()) }
single { GaanaProvider(get(),get(),get(),get()) } single { GaanaProvider(get(),get(),get(),get()) }
single { YoutubeProvider(get(),get(),get(),get()) } single { YoutubeProvider(get(),get(),get(),get()) }
single { FetchPlatformQueryResult(get(),get(),get(),get(),get()) } single { YoutubeMp3(get(),get(),get(),get()) }
single { FetchPlatformQueryResult(get(),get(),get(),get(),get(),get()) }
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) } single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
} }

View File

@ -4,6 +4,7 @@ import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.database.Database import com.shabinder.database.Database
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -14,6 +15,7 @@ class FetchPlatformQueryResult(
private val spotifyProvider: SpotifyProvider, private val spotifyProvider: SpotifyProvider,
val youtubeProvider: YoutubeProvider, val youtubeProvider: YoutubeProvider,
val youtubeMusic: YoutubeMusic, val youtubeMusic: YoutubeMusic,
val youtubeMp3: YoutubeMp3,
private val database: Database private val database: Database
) { ) {
private val db:DownloadRecordDatabaseQueries private val db:DownloadRecordDatabaseQueries

View File

@ -220,7 +220,7 @@ class GaanaProvider(
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
source = Source.Gaana, source = Source.Gaana,
albumArtURL = it.artworkLink, albumArtURL = it.artworkLink,
outputFilePath = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir(),".m4a") outputFilePath = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir()/*,".m4a"*/)
) )
} }
} }

View File

@ -271,7 +271,7 @@ class SpotifyProvider(
downloaded = it.downloaded, downloaded = it.downloaded,
source = Source.Spotify, source = Source.Spotify,
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(), albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
outputFilePath = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir(),".m4a") outputFilePath = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir()/*,".m4a"*/)
) )
} }
} }

View File

@ -0,0 +1,16 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
import com.shabinder.database.Database
import io.ktor.client.*
class YoutubeMp3(
override val httpClient: HttpClient,
private val database: Database,
private val logger: Kermit,
private val dir: Dir,
):Yt1sMp3 {
suspend fun getMp3DownloadLink(videoID:String):String? = getLinkFromYt1sMp3(videoID)
}

View File

@ -0,0 +1,46 @@
package com.shabinder.common.di.youtubeMp3
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
/*
* site link: https://yt1s.com/youtube-to-mp3/en1
* Provides Direct Mp3 , No Need For FFmpeg
* */
interface Yt1sMp3 {
val httpClient: HttpClient
/*
* Downloadable Mp3 Link for YT videoID.
* */
suspend fun getLinkFromYt1sMp3(videoID: String):String? =
getConvertedMp3Link(videoID,getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "");
/*
* POST:https://yt1s.com/api/ajaxSearch/index
* Body Form= q:yt video link ,vt:format=mp3
* */
private suspend fun getKey(videoID:String):String{
val response:JsonObject? = httpClient.post("https://yt1s.com/api/ajaxSearch/index"){
body = FormDataContent(Parameters.build {
append("q","https://www.youtube.com/watch?v=$videoID")
append("vt","mp3")
})
}
return response?.get("kc")?.jsonPrimitive.toString()
}
private suspend fun getConvertedMp3Link(videoID: String,key:String):JsonObject?{
return httpClient.post("https://yt1s.com/api/ajaxConvert/convert"){
body = FormDataContent(Parameters.build {
append("vid", videoID)
append("k",key)
})
}
}
}

View File

@ -126,7 +126,7 @@ actual class YoutubeProvider actual constructor(
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
}, },
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"), outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
videoID = it.videoId() videoID = it.videoId()
) )
} }
@ -196,7 +196,7 @@ actual class YoutubeProvider actual constructor(
else { else {
DownloadStatus.NotDownloaded DownloadStatus.NotDownloaded
}, },
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"), outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
videoID = searchId videoID = searchId
) )
) )