mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-12-25 14:07:54 +01:00
Migrating to youtube-api-dl
This commit is contained in:
parent
c9696fa4aa
commit
e3c3a6cb6c
@ -25,6 +25,7 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven(url = "https://jitpack.io")
|
||||
maven(url = "https://dl.bintray.com/ekito/koin")
|
||||
maven(url = "https://kotlin.bintray.com/kotlinx/")
|
||||
|
@ -136,7 +136,8 @@ object Ktor {
|
||||
}
|
||||
|
||||
object Extras {
|
||||
const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.1"
|
||||
//const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.5.1"
|
||||
const val youtubeDownloader = "com.shabinder.downloader:youtube-api-dl:0.1-SNAPSHOT" //Local Maven
|
||||
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}"
|
||||
|
@ -26,20 +26,17 @@ import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@ -47,7 +44,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.extensions.compose.jetbrains.Children
|
||||
|
@ -27,7 +27,7 @@ kotlin {
|
||||
dependencies {
|
||||
api("dev.icerock.moko:parcelize:0.6.0")
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3")
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,4 +20,4 @@ sealed class AllPlatforms {
|
||||
object Js : AllPlatforms()
|
||||
object Jvm : AllPlatforms()
|
||||
object Native : AllPlatforms()
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ kotlin {
|
||||
implementation(Ktor.clientLogging)
|
||||
implementation(Ktor.clientJson)
|
||||
implementation(Ktor.auth)
|
||||
api(Extras.youtubeDownloader)
|
||||
// koin
|
||||
api(Koin.core)
|
||||
api(Koin.test)
|
||||
@ -50,7 +51,6 @@ kotlin {
|
||||
implementation(Ktor.clientAndroid)
|
||||
implementation(Extras.Android.fetch)
|
||||
implementation(Extras.Android.razorpay)
|
||||
api(Extras.youtubeDownloader)
|
||||
api(Extras.mp3agic)
|
||||
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||
}
|
||||
@ -60,7 +60,6 @@ kotlin {
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(Ktor.clientApache)
|
||||
implementation(Ktor.slf4j)
|
||||
api(Extras.youtubeDownloader)
|
||||
api(Extras.mp3agic)
|
||||
}
|
||||
}
|
||||
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Shabinder Singh
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.di.utils.removeIllegalChars
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.spotify.Source
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
) {
|
||||
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||
/*
|
||||
* YT Album Art Schema
|
||||
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
* */
|
||||
private val sampleDomain1 = "music.youtube.com"
|
||||
private val sampleDomain2 = "youtube.com"
|
||||
private val sampleDomain3 = "youtu.be"
|
||||
|
||||
actual suspend fun query(fullLink: String): PlatformQueryResult? = withContext(Dispatchers.IO) {
|
||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||
// Given Link is of a Playlist
|
||||
logger.i { link }
|
||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||
getYTPlaylist(
|
||||
playlistId
|
||||
)
|
||||
} else { // Given Link is of a Video
|
||||
var searchId = "error"
|
||||
when {
|
||||
link.contains(sampleDomain1, true) -> { // Youtube Music
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&").substringAfterLast("=")
|
||||
}
|
||||
link.contains(sampleDomain2, true) -> { // Standard Youtube Link
|
||||
searchId = link.substringAfterLast("=", "error").substringBefore("&")
|
||||
}
|
||||
link.contains(sampleDomain3, true) -> { // Shortened Youtube Link
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&")
|
||||
}
|
||||
}
|
||||
if (searchId != "error") {
|
||||
getYTTrack(
|
||||
searchId
|
||||
)
|
||||
} else {
|
||||
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYTPlaylist(
|
||||
searchId: String
|
||||
): PlatformQueryResult? = withContext(Dispatchers.IO) {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
)
|
||||
result.apply {
|
||||
try {
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details()
|
||||
val name = playlistDetails.title()
|
||||
subFolder = removeIllegalChars(name)
|
||||
val videos = playlist.videos()
|
||||
|
||||
coverUrl = "https://i.ytimg.com/vi/${
|
||||
videos.firstOrNull()?.videoId()
|
||||
}/hqdefault.jpg"
|
||||
title = name
|
||||
|
||||
trackList = videos.map {
|
||||
TrackDetails(
|
||||
title = it.title(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = it.title(),
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = it.videoId()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.d { "An Error Occurred While Processing!" }
|
||||
}
|
||||
}
|
||||
if (result.title.isNotBlank()) result else null
|
||||
}
|
||||
|
||||
@Suppress("DefaultLocale")
|
||||
private suspend fun getYTTrack(
|
||||
searchId: String,
|
||||
): PlatformQueryResult? = withContext(Dispatchers.IO) {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
).apply {
|
||||
try {
|
||||
logger.i { searchId }
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
val detail = video?.details()
|
||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||
?: detail?.title() ?: ""
|
||||
// logger.i{ detail.toString() }
|
||||
trackList = listOf(
|
||||
TrackDetails(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = name,
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir = dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
title = name
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.e { "An Error Occurred While Processing!,$searchId" }
|
||||
}
|
||||
}
|
||||
if (result.title.isNotBlank()) result else null
|
||||
}
|
||||
}
|
@ -38,14 +38,14 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.shabinder.common.di.Dir
|
||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||
import com.shabinder.common.di.R
|
||||
import com.shabinder.common.di.getData
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.downloader.YoutubeDownloader
|
||||
import com.shabinder.downloader.models.formats.Format
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
@ -198,7 +198,7 @@ class ForegroundService : Service(), CoroutineScope {
|
||||
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
||||
if (url == null) {
|
||||
val audioData: Format = ytDownloader.getVideo(videoID).getData() ?: throw Exception("Java YT Dependency Error")
|
||||
val ytUrl: String = audioData.url()
|
||||
val ytUrl = audioData.url!! //We Will catch NPE
|
||||
enqueueDownload(ytUrl, track)
|
||||
} else enqueueDownload(url, track)
|
||||
} catch (e: Exception) {
|
||||
|
@ -17,13 +17,182 @@
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.di.utils.removeIllegalChars
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.spotify.Source
|
||||
import com.shabinder.downloader.YoutubeDownloader
|
||||
import com.shabinder.downloader.models.YoutubeVideo
|
||||
import com.shabinder.downloader.models.formats.Format
|
||||
import com.shabinder.downloader.models.quality.AudioQuality
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
expect class YoutubeProvider(
|
||||
httpClient: HttpClient,
|
||||
logger: Kermit,
|
||||
dir: Dir
|
||||
class YoutubeProvider(
|
||||
private val httpClient: HttpClient,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
) {
|
||||
suspend fun query(fullLink: String): PlatformQueryResult?
|
||||
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||
|
||||
/*
|
||||
* YT Album Art Schema
|
||||
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
* */
|
||||
private val sampleDomain1 = "music.youtube.com"
|
||||
private val sampleDomain2 = "youtube.com"
|
||||
private val sampleDomain3 = "youtu.be"
|
||||
|
||||
suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||
// Given Link is of a Playlist
|
||||
logger.i { link }
|
||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||
return getYTPlaylist(
|
||||
playlistId
|
||||
)
|
||||
} else { // Given Link is of a Video
|
||||
var searchId = "error"
|
||||
when {
|
||||
link.contains(sampleDomain1, true) -> { // Youtube Music
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&").substringAfterLast("=")
|
||||
}
|
||||
link.contains(sampleDomain2, true) -> { // Standard Youtube Link
|
||||
searchId = link.substringAfterLast("=", "error").substringBefore("&")
|
||||
}
|
||||
link.contains(sampleDomain3, true) -> { // Shortened Youtube Link
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&")
|
||||
}
|
||||
}
|
||||
return if (searchId != "error") {
|
||||
getYTTrack(
|
||||
searchId
|
||||
)
|
||||
} else {
|
||||
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYTPlaylist(
|
||||
searchId: String
|
||||
): PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
)
|
||||
result.apply {
|
||||
try {
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details
|
||||
val name = playlistDetails.title
|
||||
subFolder = removeIllegalChars(name)
|
||||
val videos = playlist.videos
|
||||
|
||||
coverUrl = "https://i.ytimg.com/vi/${
|
||||
videos.firstOrNull()?.videoId
|
||||
}/hqdefault.jpg"
|
||||
title = name
|
||||
|
||||
trackList = videos.map {
|
||||
TrackDetails(
|
||||
title = it.title ?: "N/A",
|
||||
artists = listOf(it.author ?: "N/A"),
|
||||
durationSec = it.lengthSeconds,
|
||||
albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = it.title ?: "N/A",
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = it.videoId
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.d { "An Error Occurred While Processing!" }
|
||||
}
|
||||
}
|
||||
return if (result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
|
||||
@Suppress("DefaultLocale")
|
||||
private suspend fun getYTTrack(
|
||||
searchId: String,
|
||||
): PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
).apply {
|
||||
try {
|
||||
logger.i { searchId }
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
val detail = video.videoDetails
|
||||
val name = detail.title?.replace(detail.author?.toUpperCase() ?: "", "", true)
|
||||
?: detail.title ?: ""
|
||||
// logger.i{ detail.toString() }
|
||||
trackList = listOf(
|
||||
TrackDetails(
|
||||
title = name,
|
||||
artists = listOf(detail.author ?: "N/A"),
|
||||
durationSec = detail.lengthSeconds,
|
||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = name,
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir = dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
title = name
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.e { "An Error Occurred While Processing!,$searchId" }
|
||||
}
|
||||
}
|
||||
return if (result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
fun YoutubeVideo.getData(): Format? {
|
||||
return getAudioWithQuality(AudioQuality.high).getOrNull(0)
|
||||
?: getAudioWithQuality(AudioQuality.medium).getOrNull(0)
|
||||
?: getAudioWithQuality(AudioQuality.low).getOrNull(0)
|
||||
}
|
||||
|
@ -16,15 +16,12 @@
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.github.kiulian.downloader.model.YoutubeVideo
|
||||
import com.github.kiulian.downloader.model.formats.Format
|
||||
import com.github.kiulian.downloader.model.quality.AudioQuality
|
||||
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.downloader.YoutubeDownloader
|
||||
import io.ktor.client.request.head
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@ -115,7 +112,7 @@ suspend fun downloadTrack(
|
||||
val audioData = ytDownloader.getVideo(videoID).getData()
|
||||
|
||||
audioData?.let { format ->
|
||||
val url: String = format.url()
|
||||
val url = format.url ?: return
|
||||
downloadFile(url).collect {
|
||||
when (it) {
|
||||
is DownloadResult.Error -> {
|
||||
@ -147,18 +144,3 @@ suspend fun downloadTrack(
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
fun YoutubeVideo.getData(): Format? {
|
||||
return try {
|
||||
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
try {
|
||||
findAudioWithQuality(AudioQuality.high)?.get(0) as Format
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
try {
|
||||
findAudioWithQuality(AudioQuality.low)?.get(0) as Format
|
||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Shabinder Singh
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.di.utils.removeIllegalChars
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.spotify.Source
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
private val httpClient: HttpClient,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
) {
|
||||
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||
|
||||
/*
|
||||
* YT Album Art Schema
|
||||
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||
* Normal Url: https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
* */
|
||||
private val sampleDomain1 = "music.youtube.com"
|
||||
private val sampleDomain2 = "youtube.com"
|
||||
private val sampleDomain3 = "youtu.be"
|
||||
|
||||
actual suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||
// Given Link is of a Playlist
|
||||
logger.i { link }
|
||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||
return getYTPlaylist(
|
||||
playlistId
|
||||
)
|
||||
} else { // Given Link is of a Video
|
||||
var searchId = "error"
|
||||
when {
|
||||
link.contains(sampleDomain1, true) -> { // Youtube Music
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&").substringAfterLast("=")
|
||||
}
|
||||
link.contains(sampleDomain2, true) -> { // Standard Youtube Link
|
||||
searchId = link.substringAfterLast("=", "error").substringBefore("&")
|
||||
}
|
||||
link.contains(sampleDomain3, true) -> { // Shortened Youtube Link
|
||||
searchId = link.substringAfterLast("/", "error").substringBefore("&")
|
||||
}
|
||||
}
|
||||
return if (searchId != "error") {
|
||||
getYTTrack(
|
||||
searchId
|
||||
)
|
||||
} else {
|
||||
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getYTPlaylist(
|
||||
searchId: String
|
||||
): PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
)
|
||||
result.apply {
|
||||
try {
|
||||
val playlist = ytDownloader.getPlaylist(searchId)
|
||||
val playlistDetails = playlist.details()
|
||||
val name = playlistDetails.title()
|
||||
subFolder = removeIllegalChars(name)
|
||||
val videos = playlist.videos()
|
||||
|
||||
coverUrl = "https://i.ytimg.com/vi/${
|
||||
videos.firstOrNull()?.videoId()
|
||||
}/hqdefault.jpg"
|
||||
title = name
|
||||
|
||||
trackList = videos.map {
|
||||
TrackDetails(
|
||||
title = it.title(),
|
||||
artists = listOf(it.author().toString()),
|
||||
durationSec = it.lengthSeconds(),
|
||||
albumArtPath = dir.imageCacheDir() + it.videoId() + ".jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = it.title(),
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = it.videoId()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.d { "An Error Occurred While Processing!" }
|
||||
}
|
||||
}
|
||||
return if (result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
|
||||
@Suppress("DefaultLocale")
|
||||
private suspend fun getYTTrack(
|
||||
searchId: String,
|
||||
): PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
).apply {
|
||||
try {
|
||||
logger.i { searchId }
|
||||
val video = ytDownloader.getVideo(searchId)
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||
val detail = video?.details()
|
||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||
?: detail?.title() ?: ""
|
||||
// logger.i{ detail.toString() }
|
||||
trackList = listOf(
|
||||
TrackDetails(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArtPath = dir.imageCacheDir() + "$searchId.jpeg",
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
itemName = name,
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir = dir.defaultDir()
|
||||
)
|
||||
)
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
title = name
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
logger.e { "An Error Occurred While Processing!,$searchId" }
|
||||
}
|
||||
}
|
||||
return if (result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* * Copyright (c) 2021 Shabinder Singh
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.shabinder.common.di
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
actual class YoutubeProvider actual constructor(
|
||||
httpClient: HttpClient,
|
||||
logger: Kermit,
|
||||
dir: Dir
|
||||
) {
|
||||
actual suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||
return null // TODO
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@
|
||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.arkivanov.decompose.DefaultComponentContext
|
||||
import com.arkivanov.decompose.lifecycle.LifecycleRegistry
|
||||
import com.arkivanov.decompose.lifecycle.destroy
|
||||
|
@ -32,7 +32,7 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
|
||||
initialState = State(routerState = props.model.routerState.value)
|
||||
) {
|
||||
private val component: Child
|
||||
get() = model.routerState.value.activeChild.component
|
||||
get() = model.routerState.value.activeChild.instance
|
||||
|
||||
private val callBacks get() = model.callBacks
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user