mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +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()
|
initialise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}"
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
|
@ -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,10 +58,11 @@ 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
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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()}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -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}")
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 {
|
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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user