mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 01:04:31 +01:00
Refactoring(WIP)
This commit is contained in:
parent
a647d1ebce
commit
be23c0e4c5
@ -11,17 +11,14 @@ import com.shabinder.common.youtube.YoutubeMusic
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : AppCompatActivity(),YoutubeMusic {
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val scope = rememberCoroutineScope()
|
||||
SpotiFlyerMain()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val token = authenticateSpotify()
|
||||
val response = getYoutubeMusicResponse("filhaal")
|
||||
Log.i("Spotify",token.toString())
|
||||
Log.i("Youtube",response)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,12 @@ android {
|
||||
minSdkVersion(Versions.minSdkVersion)
|
||||
targetSdkVersion(Versions.targetSdkVersion)
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = Versions.composeVersion
|
||||
kotlinCompilerVersion = Versions.kotlinVersion
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@ -23,7 +25,9 @@ android {
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
java.srcDirs("src/androidMain/kotlin")
|
||||
res.srcDirs("src/androidMain/res")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
named("commonMain") {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(Deps.ArkIvanov.Decompose.decompose)
|
||||
implementation(Deps.ArkIvanov.Decompose.extensionsCompose)
|
||||
|
@ -6,7 +6,7 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
named("commonMain") {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.shabinder.common
|
||||
|
||||
/**
|
||||
* Removing Illegal Chars from File Name
|
||||
* **/
|
||||
fun removeIllegalChars(fileName: String): String {
|
||||
val illegalCharArray = charArrayOf(
|
||||
'/',
|
||||
'\n',
|
||||
'\r',
|
||||
'\t',
|
||||
'\u0000',
|
||||
'\u000C',
|
||||
'`',
|
||||
'?',
|
||||
'*',
|
||||
'\\',
|
||||
'<',
|
||||
'>',
|
||||
'|',
|
||||
'\"',
|
||||
'.',
|
||||
'-',
|
||||
'\''
|
||||
)
|
||||
|
||||
var name = fileName
|
||||
for (c in illegalCharArray) {
|
||||
name = fileName.replace(c, '_')
|
||||
}
|
||||
name = name.replace("\\s".toRegex(), "_")
|
||||
name = name.replace("\\)".toRegex(), "")
|
||||
name = name.replace("\\(".toRegex(), "")
|
||||
name = name.replace("\\[".toRegex(), "")
|
||||
name = name.replace("]".toRegex(), "")
|
||||
name = name.replace("\\.".toRegex(), "")
|
||||
name = name.replace("\"".toRegex(), "")
|
||||
name = name.replace("\'".toRegex(), "")
|
||||
name = name.replace(":".toRegex(), "")
|
||||
name = name.replace("\\|".toRegex(), "")
|
||||
return name
|
||||
}
|
@ -6,7 +6,7 @@ plugins {
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
named("commonMain") {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(project(":common:data-models"))
|
||||
implementation(project(":common:database"))
|
||||
@ -20,13 +20,13 @@ kotlin {
|
||||
implementation(Ktor.auth)
|
||||
}
|
||||
}
|
||||
named("androidMain"){
|
||||
androidMain {
|
||||
dependencies{
|
||||
implementation(Ktor.clientAndroid)
|
||||
|
||||
}
|
||||
}
|
||||
named("desktopMain"){
|
||||
desktopMain {
|
||||
dependencies{
|
||||
//implementation(Ktor.clientDesktop)
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
|
||||
actual open class PlatformDir {
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
||||
|
||||
// actual fun imageDir(): String = context.cacheDir.absolutePath + File.separator
|
||||
actual fun imageDir(): String = defaultDir() + File.separator + ".images" + File.separator
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
actual fun defaultDir(): String =
|
||||
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||
Environment.DIRECTORY_MUSIC + File.separator +
|
||||
"SpotiFlyer"+ File.separator
|
||||
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.PlatformDir
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.PlatformQueryResult
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.utils.log
|
||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||
import com.shabinder.spotiflyer.utils.showDialog
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
||||
|
||||
/*
|
||||
* 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
|
||||
log("YT Play",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{
|
||||
showDialog("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
|
||||
)
|
||||
with(result) {
|
||||
try {
|
||||
log("YT Playlist", searchId)
|
||||
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(),
|
||||
albumArt = File(imageDir + it.videoId() + ".jpeg"),
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (File(
|
||||
finalOutputDir(
|
||||
itemName = it.title(),
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir
|
||||
)
|
||||
).exists()
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFile = finalOutputDir(it.title(), folderType, subFolder, defaultDir,".m4a"),
|
||||
videoID = it.videoId()
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "PlayList",
|
||||
name = if (name.length > 17) {
|
||||
"${name.subSequence(0, 16)}..."
|
||||
} else {
|
||||
name
|
||||
},
|
||||
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/${
|
||||
videos.firstOrNull()?.videoId()
|
||||
}/hqdefault.jpg",
|
||||
totalFiles = videos.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showDialog("An Error Occurred While Processing!")
|
||||
}
|
||||
}
|
||||
return if(result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private suspend fun getYTTrack(
|
||||
searchId:String,
|
||||
):PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
)
|
||||
with(result) {
|
||||
try {
|
||||
log("YT Video", 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() ?: ""
|
||||
log("YT View Model", detail.toString())
|
||||
trackList = listOf(
|
||||
TrackDetails(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArt = File(imageDir, "$searchId.jpeg"),
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (File(
|
||||
finalOutputDir(
|
||||
itemName = name,
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir = defaultDir
|
||||
)
|
||||
).exists()
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFile = finalOutputDir(name, folderType, subFolder, defaultDir,".m4a"),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
title = name
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Track",
|
||||
name = if (name.length > 17) {
|
||||
"${name.subSequence(0, 16)}..."
|
||||
} else {
|
||||
name
|
||||
},
|
||||
link = "https://www.youtube.com/watch?v=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
totalFiles = 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showDialog("An Error Occurred While Processing!,$searchId")
|
||||
}
|
||||
}
|
||||
return if(result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
}
|
@ -10,7 +10,5 @@ import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
import org.kodein.di.DI
|
||||
|
||||
val networking = DI.Module("Networking"){
|
||||
|
||||
}
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.singleton
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.shabinder.common
|
||||
|
||||
expect open class PlatformDir() {
|
||||
fun isPresent(path:String):Boolean
|
||||
fun fileSeparator(): String
|
||||
fun defaultDir(): String
|
||||
fun imageDir(): String
|
||||
}
|
||||
|
||||
fun PlatformDir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
||||
removeIllegalChars(itemName) + extension
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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.providers
|
||||
|
||||
import com.shabinder.common.*
|
||||
import com.shabinder.common.gaana.GaanaRequests
|
||||
import com.shabinder.common.gaana.GaanaTrack
|
||||
import com.shabinder.common.spotify.Source
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class GaanaProvider: PlatformDir(),GaanaRequests {
|
||||
|
||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||
|
||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||
//Link Schema: https://gaana.com/type/link
|
||||
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
||||
|
||||
val link = gaanaLink.substringAfterLast('/', "error")
|
||||
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
|
||||
|
||||
//Error
|
||||
if (type == "Error" || link == "Error"){
|
||||
return null
|
||||
}
|
||||
return gaanaSearch(
|
||||
type,
|
||||
link
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun gaanaSearch(
|
||||
type:String,
|
||||
link:String,
|
||||
): PlatformQueryResult {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = link,
|
||||
title = link,
|
||||
coverUrl = gaanaPlaceholderImageUrl,
|
||||
trackList = listOf(),
|
||||
Source.Gaana
|
||||
)
|
||||
with(result) {
|
||||
when (type) {
|
||||
"song" -> {
|
||||
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
||||
folderType = "Tracks"
|
||||
subFolder = ""
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
it.track_title,
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)) {//Download Already Present!!
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||
title = it.track_title
|
||||
coverUrl = it.artworkLink
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Track",
|
||||
name = title,
|
||||
link = "https://gaana.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
"album" -> {
|
||||
getGaanaAlbum(seokey = link).also {
|
||||
folderType = "Albums"
|
||||
subFolder = link
|
||||
it.tracks.forEach { track ->
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
track.track_title,
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
track.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
}
|
||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||
title = link
|
||||
coverUrl = it.custom_artworks.size_480p
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Album",
|
||||
name = title,
|
||||
link = "https://gaana.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = trackList.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
"playlist" -> {
|
||||
getGaanaPlaylist(seokey = link).also {
|
||||
folderType = "Playlists"
|
||||
subFolder = link
|
||||
it.tracks.forEach { track ->
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
track.track_title,
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
track.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
}
|
||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||
title = link
|
||||
//coverUrl.value = "TODO"
|
||||
coverUrl = gaanaPlaceholderImageUrl
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Playlist",
|
||||
name = title,
|
||||
link = "https://gaana.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = it.tracks.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
"artist" -> {
|
||||
folderType = "Artist"
|
||||
subFolder = link
|
||||
coverUrl = gaanaPlaceholderImageUrl
|
||||
val artistDetails =
|
||||
getGaanaArtistDetails(seokey = link).artist?.firstOrNull()
|
||||
?.also {
|
||||
title = it.name
|
||||
coverUrl = it.artworkLink ?: gaanaPlaceholderImageUrl
|
||||
}
|
||||
getGaanaArtistTracks(seokey = link).also {
|
||||
it.tracks.forEach { track ->
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
track.track_title,
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
track.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
}
|
||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Artist",
|
||||
name = artistDetails?.name ?: link,
|
||||
link = "https://gaana.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = trackList.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {//TODO Handle Error}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<GaanaTrack>.toTrackDetailsList(type:String, subFolder:String) = this.map {
|
||||
TrackDetails(
|
||||
title = it.track_title,
|
||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||
durationSec = it.duration,
|
||||
// albumArt = File(
|
||||
// imageDir + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
|
||||
albumName = it.album_title,
|
||||
year = it.release_date,
|
||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||
trackUrl = it.lyrics_url,
|
||||
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
||||
source = Source.Gaana,
|
||||
albumArtURL = it.artworkLink,
|
||||
outputFile = finalOutputDir(it.track_title,type, subFolder,defaultDir(),".m4a")
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.providers
|
||||
|
||||
import com.shabinder.common.*
|
||||
import com.shabinder.common.spotify.*
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
||||
|
||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||
var spotifyLink =
|
||||
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
|
||||
|
||||
if (!spotifyLink.contains("open.spotify")) {
|
||||
//Very Rare instance
|
||||
spotifyLink = resolveLink(spotifyLink)
|
||||
}
|
||||
|
||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||
|
||||
|
||||
if (type == "Error" || link == "Error") {
|
||||
return null
|
||||
}
|
||||
|
||||
if (type == "episode" || type == "show") {
|
||||
//TODO Implementation
|
||||
return null
|
||||
}
|
||||
|
||||
return spotifySearch(
|
||||
type,
|
||||
link
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun spotifySearch(
|
||||
type:String,
|
||||
link: String
|
||||
): PlatformQueryResult {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.Spotify
|
||||
)
|
||||
with(result) {
|
||||
when (type) {
|
||||
"track" -> {
|
||||
getTrack(link).also {
|
||||
folderType = "Tracks"
|
||||
subFolder = ""
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
it.name.toString(),
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||
title = it.name.toString()
|
||||
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
|
||||
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Track",
|
||||
name = title,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"album" -> {
|
||||
val albumObject = getAlbum(link)
|
||||
folderType = "Albums"
|
||||
subFolder = albumObject?.name.toString()
|
||||
albumObject?.tracks?.items?.forEach {
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
it.name.toString(),
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
it.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
it.album = Album(
|
||||
images = listOf(
|
||||
Image(
|
||||
url = albumObject.images?.elementAtOrNull(1)?.url
|
||||
?: albumObject.images?.elementAtOrNull(0)?.url
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
albumObject.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {it ->
|
||||
if (it.isNullOrEmpty()) {
|
||||
//TODO Handle Error
|
||||
} else {
|
||||
trackList = it
|
||||
title = albumObject.name.toString()
|
||||
coverUrl = (albumObject.images?.elementAtOrNull(1)?.url
|
||||
?: albumObject.images?.elementAtOrNull(0)?.url).toString()
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Album",
|
||||
name = title,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = trackList.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"playlist" -> {
|
||||
val playlistObject = getPlaylist(link)
|
||||
folderType = "Playlists"
|
||||
subFolder = playlistObject.name.toString()
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
//log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
||||
playlistObject?.tracks?.items?.forEach {
|
||||
it.track?.let { it1 ->
|
||||
if (isPresent(
|
||||
finalOutputDir(
|
||||
it1.name.toString(),
|
||||
folderType,
|
||||
subFolder,
|
||||
defaultDir()
|
||||
)
|
||||
)
|
||||
) {//Download Already Present!!
|
||||
it1.downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
tempTrackList.add(it1)
|
||||
}
|
||||
}
|
||||
var moreTracksAvailable = !playlistObject.tracks?.next.isNullOrBlank()
|
||||
|
||||
while (moreTracksAvailable) {
|
||||
//Check For More Tracks If available
|
||||
val moreTracks =
|
||||
getPlaylistTracks(link, offset = tempTrackList.size)
|
||||
moreTracks.items?.forEach {
|
||||
it.track?.let { it1 -> tempTrackList.add(it1) }
|
||||
}
|
||||
moreTracksAvailable = !moreTracks.next.isNullOrBlank()
|
||||
}
|
||||
//log("Total Tracks Fetched", tempTrackList.size.toString())
|
||||
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder)
|
||||
title = playlistObject.name.toString()
|
||||
coverUrl = playlistObject.images?.elementAtOrNull(1)?.url
|
||||
?: playlistObject.images?.firstOrNull()?.url.toString()
|
||||
withContext(Dispatchers.Default) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Playlist",
|
||||
name = title,
|
||||
link = "https://open.spotify.com/$type/$link",
|
||||
coverUrl = coverUrl,
|
||||
totalFiles = tempTrackList.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
"episode" -> {//TODO
|
||||
}
|
||||
"show" -> {//TODO
|
||||
}
|
||||
else -> {
|
||||
//TODO Handle Error
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
* New Link Schema: https://link.tospotify.com/kqTBblrjQbb,
|
||||
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&_branch_match_id=862039436205270630
|
||||
* */
|
||||
private suspend fun resolveLink(
|
||||
url:String
|
||||
):String {
|
||||
val response = getResponse(url)
|
||||
val regex = """https://open\.spotify\.com.+\w""".toRegex()
|
||||
return regex.find(response)?.value.toString()
|
||||
}
|
||||
|
||||
private fun List<Track>.toTrackDetailsList(type:String, subFolder:String) = this.map {
|
||||
TrackDetails(
|
||||
title = it.name.toString(),
|
||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||
durationSec = (it.duration_ms/1000).toInt(),
|
||||
// albumArt = File(
|
||||
// imageDir + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
|
||||
albumName = it.album?.name,
|
||||
year = it.album?.release_date,
|
||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||
trackUrl = it.href,
|
||||
downloaded = it.downloaded,
|
||||
source = Source.Spotify,
|
||||
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
|
||||
outputFile = finalOutputDir(it.name.toString(),type, subFolder,defaultDir(),".m4a")
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.providers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.beust.klaxon.JsonArray
|
||||
import com.beust.klaxon.JsonObject
|
||||
import com.beust.klaxon.Parser
|
||||
import com.shabinder.common.YoutubeTrack
|
||||
import com.shabinder.spotiflyer.utils.log
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/*
|
||||
* Thanks To https://github.com/spotDL/spotify-downloader
|
||||
* */
|
||||
fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||
|
||||
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
||||
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
||||
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||
if (contentBlocks != null) {
|
||||
for (cBlock in contentBlocks){
|
||||
/**
|
||||
*Ignore user-suggestion
|
||||
*The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
|
||||
*results for xyz, search for abc instead') we have no use for them, the for
|
||||
*loop below if throw a keyError if we don't ignore them
|
||||
*/
|
||||
if(cBlock.containsKey("itemSectionRenderer")){
|
||||
continue
|
||||
}
|
||||
|
||||
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
||||
/**
|
||||
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||
* I have no clue what they are and why there even exist
|
||||
*
|
||||
if(!contents.containsKey("overlay")){
|
||||
println(contents)
|
||||
continue
|
||||
TODO check and correct
|
||||
}*/
|
||||
|
||||
val result = contents.obj("musicResponsiveListItemRenderer")
|
||||
?.array<JsonObject>("flexColumns")
|
||||
|
||||
//Add the linkBlock
|
||||
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
||||
?.obj("overlay")
|
||||
?.obj("musicItemThumbnailOverlayRenderer")
|
||||
?.obj("content")
|
||||
?.obj("musicPlayButtonRenderer")
|
||||
?.obj("playNavigationEndpoint")
|
||||
|
||||
// detailsBlock is always a list, so we just append the linkBlock to it
|
||||
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
||||
linkBlock?.let { result?.add(it) }
|
||||
result?.let { resultBlocks.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
||||
! Songs and Videos are supplied with different details, extracting all details from
|
||||
! both is just carrying on redundant data, so we also have to selectively extract
|
||||
! relevant details. What you need to know to understand how we do that here:
|
||||
!
|
||||
! Songs details are ALWAYS in the following order:
|
||||
! 0 - Name
|
||||
! 1 - Type (Song)
|
||||
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
||||
! 3 - Album
|
||||
! 4 - Duration (mm:ss)
|
||||
!
|
||||
! Video details are ALWAYS in the following order:
|
||||
! 0 - Name
|
||||
! 1 - Type (Video)
|
||||
! 2 - Channel
|
||||
! 3 - Viewers
|
||||
! 4 - Duration (hh:mm:ss)
|
||||
!
|
||||
! We blindly gather all the details we get our hands on, then
|
||||
! cherrypick the details we need based on their index numbers,
|
||||
! we do so only if their Type is 'Song' or 'Video
|
||||
*/
|
||||
|
||||
for(result in resultBlocks){
|
||||
|
||||
// Blindly gather available details
|
||||
val availableDetails = mutableListOf<String>()
|
||||
|
||||
/*
|
||||
Filter Out dummies here itself
|
||||
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
||||
! I have no clue. We skip these.
|
||||
|
||||
! Remember that we appended the linkBlock to result, treating that like the
|
||||
! other constituents of a result block will lead to errors, hence the 'in
|
||||
! result[:-1] ,i.e., skip last element in array '
|
||||
*/
|
||||
for(detail in result.subList(0,result.size-1)){
|
||||
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||
|
||||
// if not a dummy, collect All Variables
|
||||
val details = detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
||||
?.obj("text")
|
||||
?.array<JsonObject>("runs") ?: listOf()
|
||||
for (d in details){
|
||||
d["text"]?.let {
|
||||
if(it.toString() != " • "){
|
||||
availableDetails.add(
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// log("YT Music details",availableDetails.toString())
|
||||
/*
|
||||
! Filter Out non-Song/Video results and incomplete results here itself
|
||||
! From what we know about detail order, note that [1] - indicate result type
|
||||
*/
|
||||
if ( availableDetails.size == 5 && availableDetails[1] in listOf("Song","Video") ){
|
||||
|
||||
// skip if result is in hours instead of minutes (no song is that long)
|
||||
if(availableDetails[4].split(':').size != 2) continue
|
||||
|
||||
/*
|
||||
! grab Video ID
|
||||
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||
! so hardcoding the dict keys for data look up is an ardours process, since
|
||||
! the sub-block pattern is fixed even though the key isn't, we just
|
||||
! reference the dict keys by index
|
||||
*/
|
||||
|
||||
val videoId:String? = result.last().obj("watchEndpoint")?.get("videoId") as String?
|
||||
val ytTrack = YoutubeTrack(
|
||||
name = availableDetails[0],
|
||||
type = availableDetails[1],
|
||||
artist = availableDetails[2],
|
||||
duration = availableDetails[4],
|
||||
videoId = videoId
|
||||
)
|
||||
youtubeTracks.add(ytTrack)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
log("YT Search",youtubeTracks.joinToString(" abc \n"))
|
||||
return youtubeTracks
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun sortByBestMatch(ytTracks:List<YoutubeTrack>,
|
||||
trackName:String,
|
||||
trackArtists:List<String>,
|
||||
trackDurationSec:Int,
|
||||
):Map<String,Int>{
|
||||
/*
|
||||
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
|
||||
**/
|
||||
val linksWithMatchValue = mutableMapOf<String,Int>()
|
||||
|
||||
for (result in ytTracks){
|
||||
|
||||
// LoweCasing Name to match Properly
|
||||
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||
var hasCommonWord = false
|
||||
|
||||
val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: ""
|
||||
val trackNameWords = trackName.toLowerCase().split(" ")
|
||||
|
||||
for (nameWord in trackNameWords){
|
||||
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true
|
||||
}
|
||||
|
||||
// Skip this Result if No Word is Common in Name
|
||||
if (!hasCommonWord) {
|
||||
//log("YT Api Removing", result.toString())
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Find artist match
|
||||
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
|
||||
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
|
||||
var artistMatchNumber = 0
|
||||
|
||||
if(result.type == "Song"){
|
||||
for (artist in trackArtists){
|
||||
if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase()) > 85)
|
||||
artistMatchNumber++
|
||||
}
|
||||
}else{//i.e. is a Video
|
||||
for (artist in trackArtists) {
|
||||
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
||||
artistMatchNumber++
|
||||
}
|
||||
}
|
||||
|
||||
if(artistMatchNumber == 0) {
|
||||
//log("YT Api Removing", result.toString())
|
||||
continue
|
||||
}
|
||||
|
||||
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
||||
|
||||
// Duration Match
|
||||
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||
! difference in song duration (delta) is usually of the magnitude of a few
|
||||
! seconds, we need to amplify the delta if it is to have any meaningful impact
|
||||
! wen we calculate the avg match value*/
|
||||
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
|
||||
?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0)
|
||||
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
||||
val durationMatch = 100 - (nonMatchValue*100)
|
||||
|
||||
val avgMatch = (artistMatch + durationMatch)/2
|
||||
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||
}
|
||||
//log("YT Api Result", "$trackName - $linksWithMatchValue")
|
||||
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||
}
|
@ -44,4 +44,7 @@ interface SpotifyRequests {
|
||||
return spotifyRequestsClient.get("$BASE_URL/albums/$id")
|
||||
}
|
||||
|
||||
suspend fun getResponse(url:String):String{
|
||||
return spotifyRequestsClient.get(url)
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.shabinder.common
|
||||
|
||||
import java.io.File
|
||||
|
||||
actual open class PlatformDir{
|
||||
|
||||
actual fun fileSeparator(): String = File.separator
|
||||
|
||||
actual fun imageDir(): String = System.getProperty("user.home") + ".images" + File.separator
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() +
|
||||
"SpotiFlyer" + fileSeparator()
|
||||
|
||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||
|
||||
}
|
@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.github.kiulian.downloader.YoutubeDownloader
|
||||
import com.shabinder.common.PlatformDir
|
||||
import com.shabinder.common.providers.BaseProvider
|
||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
||||
import com.shabinder.spotiflyer.models.PlatformQueryResult
|
||||
import com.shabinder.spotiflyer.models.TrackDetails
|
||||
import com.shabinder.spotiflyer.models.spotify.Source
|
||||
import com.shabinder.spotiflyer.utils.log
|
||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
||||
import com.shabinder.spotiflyer.utils.showDialog
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
||||
|
||||
/*
|
||||
* 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"
|
||||
|
||||
override 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
|
||||
log("YT Play",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{
|
||||
showDialog("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
|
||||
)
|
||||
with(result) {
|
||||
try {
|
||||
log("YT Playlist", searchId)
|
||||
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(),
|
||||
albumArt = File(imageDir + it.videoId() + ".jpeg"),
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||
downloaded = if (File(
|
||||
finalOutputDir(
|
||||
itemName = it.title(),
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir
|
||||
)
|
||||
).exists()
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFile = finalOutputDir(it.title(), folderType, subFolder, defaultDir,".m4a"),
|
||||
videoID = it.videoId()
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "PlayList",
|
||||
name = if (name.length > 17) {
|
||||
"${name.subSequence(0, 16)}..."
|
||||
} else {
|
||||
name
|
||||
},
|
||||
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/${
|
||||
videos.firstOrNull()?.videoId()
|
||||
}/hqdefault.jpg",
|
||||
totalFiles = videos.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showDialog("An Error Occurred While Processing!")
|
||||
}
|
||||
}
|
||||
return if(result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private suspend fun getYTTrack(
|
||||
searchId:String,
|
||||
):PlatformQueryResult? {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.YouTube
|
||||
)
|
||||
with(result) {
|
||||
try {
|
||||
log("YT Video", 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() ?: ""
|
||||
log("YT View Model", detail.toString())
|
||||
trackList = listOf(
|
||||
TrackDetails(
|
||||
title = name,
|
||||
artists = listOf(detail?.author().toString()),
|
||||
durationSec = detail?.lengthSeconds() ?: 0,
|
||||
albumArt = File(imageDir, "$searchId.jpeg"),
|
||||
source = Source.YouTube,
|
||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
downloaded = if (File(
|
||||
finalOutputDir(
|
||||
itemName = name,
|
||||
type = folderType,
|
||||
subFolder = subFolder,
|
||||
defaultDir = defaultDir
|
||||
)
|
||||
).exists()
|
||||
)
|
||||
DownloadStatus.Downloaded
|
||||
else {
|
||||
DownloadStatus.NotDownloaded
|
||||
},
|
||||
outputFile = finalOutputDir(name, folderType, subFolder, defaultDir,".m4a"),
|
||||
videoID = searchId
|
||||
)
|
||||
)
|
||||
title = name
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
databaseDAO.insert(
|
||||
DownloadRecord(
|
||||
type = "Track",
|
||||
name = if (name.length > 17) {
|
||||
"${name.subSequence(0, 16)}..."
|
||||
} else {
|
||||
name
|
||||
},
|
||||
link = "https://www.youtube.com/watch?v=$searchId",
|
||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||
totalFiles = 1,
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showDialog("An Error Occurred While Processing!,$searchId")
|
||||
}
|
||||
}
|
||||
return if(result.title.isNotBlank()) result
|
||||
else null
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user