mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 02:14:32 +01:00
Migrating to youtube-api-dl
This commit is contained in:
parent
c9696fa4aa
commit
e3c3a6cb6c
@ -25,6 +25,7 @@ allprojects {
|
|||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
maven(url = "https://dl.bintray.com/ekito/koin")
|
maven(url = "https://dl.bintray.com/ekito/koin")
|
||||||
maven(url = "https://kotlin.bintray.com/kotlinx/")
|
maven(url = "https://kotlin.bintray.com/kotlinx/")
|
||||||
|
@ -136,7 +136,8 @@ object Ktor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Extras {
|
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 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}"
|
||||||
|
@ -26,20 +26,17 @@ import androidx.compose.animation.core.spring
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
import androidx.compose.material.ripple.RippleAlpha
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -47,7 +44,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.Children
|
import com.arkivanov.decompose.extensions.compose.jetbrains.Children
|
||||||
|
@ -27,7 +27,7 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
api("dev.icerock.moko:parcelize:0.6.0")
|
api("dev.icerock.moko:parcelize:0.6.0")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ kotlin {
|
|||||||
implementation(Ktor.clientLogging)
|
implementation(Ktor.clientLogging)
|
||||||
implementation(Ktor.clientJson)
|
implementation(Ktor.clientJson)
|
||||||
implementation(Ktor.auth)
|
implementation(Ktor.auth)
|
||||||
|
api(Extras.youtubeDownloader)
|
||||||
// koin
|
// koin
|
||||||
api(Koin.core)
|
api(Koin.core)
|
||||||
api(Koin.test)
|
api(Koin.test)
|
||||||
@ -50,7 +51,6 @@ kotlin {
|
|||||||
implementation(Ktor.clientAndroid)
|
implementation(Ktor.clientAndroid)
|
||||||
implementation(Extras.Android.fetch)
|
implementation(Extras.Android.fetch)
|
||||||
implementation(Extras.Android.razorpay)
|
implementation(Extras.Android.razorpay)
|
||||||
api(Extras.youtubeDownloader)
|
|
||||||
api(Extras.mp3agic)
|
api(Extras.mp3agic)
|
||||||
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||||
}
|
}
|
||||||
@ -60,7 +60,6 @@ kotlin {
|
|||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(Ktor.clientApache)
|
implementation(Ktor.clientApache)
|
||||||
implementation(Ktor.slf4j)
|
implementation(Ktor.slf4j)
|
||||||
api(Extras.youtubeDownloader)
|
|
||||||
api(Extras.mp3agic)
|
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.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import co.touchlab.kermit.Kermit
|
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.Dir
|
||||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
import com.shabinder.common.di.R
|
import com.shabinder.common.di.R
|
||||||
import com.shabinder.common.di.getData
|
import com.shabinder.common.di.getData
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
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.Download
|
||||||
import com.tonyodev.fetch2.Error
|
import com.tonyodev.fetch2.Error
|
||||||
import com.tonyodev.fetch2.Fetch
|
import com.tonyodev.fetch2.Fetch
|
||||||
@ -198,7 +198,7 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
val audioData: Format = ytDownloader.getVideo(videoID).getData() ?: throw Exception("Java YT Dependency Error")
|
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)
|
enqueueDownload(ytUrl, track)
|
||||||
} else enqueueDownload(url, track)
|
} else enqueueDownload(url, track)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -17,13 +17,182 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
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.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
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
expect class YoutubeProvider(
|
class YoutubeProvider(
|
||||||
httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
logger: Kermit,
|
private val logger: Kermit,
|
||||||
dir: Dir
|
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
|
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.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
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
import com.shabinder.downloader.YoutubeDownloader
|
||||||
import io.ktor.client.request.head
|
import io.ktor.client.request.head
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@ -115,7 +112,7 @@ suspend fun downloadTrack(
|
|||||||
val audioData = ytDownloader.getVideo(videoID).getData()
|
val audioData = ytDownloader.getVideo(videoID).getData()
|
||||||
|
|
||||||
audioData?.let { format ->
|
audioData?.let { format ->
|
||||||
val url: String = format.url()
|
val url = format.url ?: return
|
||||||
downloadFile(url).collect {
|
downloadFile(url).collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
@ -147,18 +144,3 @@ suspend fun downloadTrack(
|
|||||||
e.printStackTrace()
|
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/>.
|
* * 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.DefaultComponentContext
|
||||||
import com.arkivanov.decompose.lifecycle.LifecycleRegistry
|
import com.arkivanov.decompose.lifecycle.LifecycleRegistry
|
||||||
import com.arkivanov.decompose.lifecycle.destroy
|
import com.arkivanov.decompose.lifecycle.destroy
|
||||||
|
@ -32,7 +32,7 @@ class RootR(props: Props<SpotiFlyerRoot>) : RenderableRootComponent<SpotiFlyerRo
|
|||||||
initialState = State(routerState = props.model.routerState.value)
|
initialState = State(routerState = props.model.routerState.value)
|
||||||
) {
|
) {
|
||||||
private val component: Child
|
private val component: Child
|
||||||
get() = model.routerState.value.activeChild.component
|
get() = model.routerState.value.activeChild.instance
|
||||||
|
|
||||||
private val callBacks get() = model.callBacks
|
private val callBacks get() = model.callBacks
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user