mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-12-22 20:57:54 +01:00
Yt Api for Mp3 , Loading Anim
This commit is contained in:
parent
189441111b
commit
760ffae8f3
@ -60,6 +60,10 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
/*lifecycleScope.launch {
|
||||
val string = fetcher.youtubeMp3.getMp3DownloadLink("lVfVrqu1G0U")
|
||||
Log.i("Mp3Test",string)
|
||||
}*/
|
||||
initialise()
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ object Ktor {
|
||||
}
|
||||
|
||||
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 mp3agic = "com.mpatric:mp3agic:0.9.1"
|
||||
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
||||
|
@ -58,10 +58,7 @@ interface SpotiFlyerList {
|
||||
object Finished : Output()
|
||||
}
|
||||
data class State(
|
||||
val queryResult: PlatformQueryResult? = PlatformQueryResult(
|
||||
"","",
|
||||
"Loading","", emptyList(),
|
||||
Source.Spotify),
|
||||
val queryResult: PlatformQueryResult? = null,
|
||||
val link:String = "",
|
||||
val trackList:List<TrackDetails> = emptyList()
|
||||
)
|
||||
|
@ -21,11 +21,6 @@ import com.shabinder.common.ui.*
|
||||
import com.shabinder.common.ui.SpotiFlyerTypography
|
||||
import com.shabinder.common.ui.colorAccent
|
||||
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
|
||||
fun SpotiFlyerListContent(
|
||||
@ -38,28 +33,35 @@ fun SpotiFlyerListContent(
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
//TODO Better Null Handling
|
||||
val result = model.queryResult!!
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = {
|
||||
item {
|
||||
CoverImage(result.title, result.coverUrl, coroutineScope,component::loadImage)
|
||||
}
|
||||
itemsIndexed(model.trackList) { index, item ->
|
||||
TrackCard(
|
||||
track = item,
|
||||
downloadTrack = { component.onDownloadClicked(item) },
|
||||
loadImage = component::loadImage
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
DownloadAllButton(
|
||||
onClick = {component.onDownloadAllClicked(result.trackList)},
|
||||
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
||||
)
|
||||
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(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = {
|
||||
item {
|
||||
CoverImage(result.title, result.coverUrl, coroutineScope,component::loadImage)
|
||||
}
|
||||
itemsIndexed(model.trackList) { index, item ->
|
||||
TrackCard(
|
||||
track = item,
|
||||
downloadTrack = { component.onDownloadClicked(item) },
|
||||
loadImage = component::loadImage
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
DownloadAllButton(
|
||||
onClick = {component.onDownloadAllClicked(model.trackList)},
|
||||
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,9 @@ internal class SpotiFlyerListStoreProvider(
|
||||
for(newTrack in map){
|
||||
titleList.indexOf(newTrack.key).let { position ->
|
||||
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
|
||||
logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update")
|
||||
//logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -298,7 +298,7 @@ fun HistoryColumn(
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = {
|
||||
items(list) {
|
||||
items(list.distinctBy { it.coverUrl }) {
|
||||
DownloadRecordItem(
|
||||
item = it,
|
||||
loadImage,
|
||||
|
@ -35,6 +35,7 @@ data class TrackDetails(
|
||||
var albumArtPath: String,
|
||||
var albumArtURL: String,
|
||||
var source: Source,
|
||||
val progress: Int = 2,
|
||||
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||
var outputFilePath: String,
|
||||
var videoID:String? = null,
|
||||
|
@ -34,7 +34,7 @@ kotlin {
|
||||
implementation(Ktor.clientAndroid)
|
||||
implementation(Extras.Android.fetch)
|
||||
implementation(Koin.android)
|
||||
api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||
//api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||
}
|
||||
}
|
||||
desktopMain {
|
||||
|
@ -8,8 +8,6 @@ import android.os.Environment
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.arthenica.mobileffmpeg.Config
|
||||
import com.arthenica.mobileffmpeg.FFmpeg
|
||||
import com.mpatric.mp3agic.Mp3File
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.database.appContext
|
||||
@ -78,33 +76,53 @@ actual class Dir actual constructor(
|
||||
mp3ByteArray: ByteArray,
|
||||
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
|
||||
* */
|
||||
if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
|
||||
//if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
|
||||
|
||||
FFmpeg.executeAsync(
|
||||
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
||||
){ _, returnCode ->
|
||||
when (returnCode) {
|
||||
Config.RETURN_CODE_SUCCESS -> {
|
||||
//FFMPEG task Completed
|
||||
logger.d{ "Async command execution completed successfully." }
|
||||
scope.launch {
|
||||
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
||||
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"}"
|
||||
){ _, returnCode ->
|
||||
when (returnCode) {
|
||||
Config.RETURN_CODE_SUCCESS -> {
|
||||
//FFMPEG task Completed
|
||||
logger.d{ "Async command execution completed successfully." }
|
||||
scope.launch {
|
||||
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
|
||||
.removeAllTags()
|
||||
.setId3v1Tags(trackDetails)
|
||||
.setId3v2TagsAndSaveFile(trackDetails)
|
||||
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
||||
}
|
||||
}
|
||||
Config.RETURN_CODE_CANCEL -> {
|
||||
logger.d{"Async command execution cancelled by user."}
|
||||
}
|
||||
else -> {
|
||||
logger.d { "Async command execution failed with rc=$returnCode" }
|
||||
}
|
||||
}
|
||||
}
|
||||
Config.RETURN_CODE_CANCEL -> {
|
||||
logger.d{"Async command execution cancelled by user."}
|
||||
}
|
||||
else -> {
|
||||
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()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ actual class YoutubeProvider actual constructor(
|
||||
else {
|
||||
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()
|
||||
)
|
||||
}
|
||||
@ -195,7 +195,7 @@ actual class YoutubeProvider actual constructor(
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
|
@ -178,11 +178,17 @@ class ForegroundService : Service(),CoroutineScope{
|
||||
private fun downloadTrack(videoID:String, track: TrackDetails){
|
||||
launch {
|
||||
try {
|
||||
val audioData = ytDownloader.getVideo(videoID).getData()
|
||||
/*val audioData = ytDownloader.getVideo(videoID).getData()
|
||||
|
||||
audioData?.let {
|
||||
val url: String = it.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{
|
||||
priority = Priority.NORMAL
|
||||
networkType = NetworkType.ALL
|
||||
@ -260,6 +266,7 @@ class ForegroundService : Service(),CoroutineScope{
|
||||
addToNotification("Processing ${it.title}")
|
||||
job.invokeOnCompletion { _ ->
|
||||
converted++
|
||||
allTracksStatus[it.title] = DownloadStatus.Downloaded
|
||||
sendTrackBroadcast(Status.COMPLETED.name,it)
|
||||
removeFromNotification("Processing ${it.title}")
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.shabinder.common.database.createDatabase
|
||||
import com.shabinder.common.database.getLogger
|
||||
import com.shabinder.common.di.providers.GaanaProvider
|
||||
import com.shabinder.common.di.providers.SpotifyProvider
|
||||
import com.shabinder.common.di.providers.YoutubeMp3
|
||||
import com.shabinder.common.di.providers.YoutubeMusic
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.features.json.*
|
||||
@ -33,7 +34,8 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||
single { SpotifyProvider(get(),get(),get(),get()) }
|
||||
single { GaanaProvider(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) }
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
import com.shabinder.common.di.providers.GaanaProvider
|
||||
import com.shabinder.common.di.providers.SpotifyProvider
|
||||
import com.shabinder.common.di.providers.YoutubeMp3
|
||||
import com.shabinder.common.di.providers.YoutubeMusic
|
||||
import com.shabinder.database.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -14,6 +15,7 @@ class FetchPlatformQueryResult(
|
||||
private val spotifyProvider: SpotifyProvider,
|
||||
val youtubeProvider: YoutubeProvider,
|
||||
val youtubeMusic: YoutubeMusic,
|
||||
val youtubeMp3: YoutubeMp3,
|
||||
private val database: Database
|
||||
) {
|
||||
private val db:DownloadRecordDatabaseQueries
|
||||
|
@ -12,10 +12,10 @@ interface GaanaRequests {
|
||||
val httpClient:HttpClient
|
||||
|
||||
/*
|
||||
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||
*
|
||||
* subtype : ["most_popular_playlist" , "playlist_home_featured" ,"playlist_detail" ,"user_playlist" ,"topCharts"]
|
||||
**/
|
||||
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||
*
|
||||
* subtype : ["most_popular_playlist" , "playlist_home_featured" ,"playlist_detail" ,"user_playlist" ,"topCharts"]
|
||||
**/
|
||||
suspend fun getGaanaPlaylist(
|
||||
type: String = "playlist",
|
||||
subtype: String = "playlist_detail",
|
||||
|
@ -220,7 +220,7 @@ class GaanaProvider(
|
||||
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
||||
source = Source.Gaana,
|
||||
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"*/)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +271,7 @@ class SpotifyProvider(
|
||||
downloaded = it.downloaded,
|
||||
source = Source.Spotify,
|
||||
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"*/)
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -126,7 +126,7 @@ actual class YoutubeProvider actual constructor(
|
||||
else {
|
||||
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()
|
||||
)
|
||||
}
|
||||
@ -196,7 +196,7 @@ actual class YoutubeProvider actual constructor(
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user