320Kbps and JioSaavn Support

This commit is contained in:
shabinder 2021-05-26 04:02:21 +05:30
parent ec8f77d121
commit 3913bfa4b1
17 changed files with 514 additions and 393 deletions

View File

@ -71,11 +71,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data
android:name="com.razorpay.ApiKey"
android:value="rzp_live_3ZQeoFYOxjmXye"
/>
<service android:name="com.shabinder.common.di.worker.ForegroundService"/> <service android:name="com.shabinder.common.di.worker.ForegroundService"/>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,24 @@
package com.shabinder.common.models
enum class AudioQuality(val kbps: String) {
KBPS128("128"),
KBPS160("160"),
KBPS192("192"),
KBPS224("224"),
KBPS256("256"),
KBPS320("320");
companion object {
fun getQuality(kbps: String): AudioQuality {
return when (kbps) {
"128" -> KBPS128
"160" -> KBPS160
"192" -> KBPS192
"224" -> KBPS224
"256" -> KBPS256
"320" -> KBPS320
else -> KBPS160 // Use 160 as baseline
}
}
}
}

View File

@ -7,7 +7,7 @@ import kotlinx.serialization.json.JsonNames
@Serializable @Serializable
data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor( data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
@JsonNames("320kbps") val is320kbps: Boolean = false, @JsonNames("320kbps") val is320Kbps: Boolean,
val album: String, val album: String,
val album_url: String? = null, val album_url: String? = null,
val albumid: String? = null, val albumid: String? = null,
@ -23,8 +23,8 @@ data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
val label: String? = null, val label: String? = null,
val label_url: String? = null, val label_url: String? = null,
val language: String, val language: String,
val lyrics: String? = null,
val lyrics_snippet: String? = null, val lyrics_snippet: String? = null,
val lyrics: String? = null,
val media_preview_url: String? = null, val media_preview_url: String? = null,
val media_url: String? = null, // Downloadable M4A Link val media_url: String? = null, // Downloadable M4A Link
val music: String, val music: String,

View File

@ -37,13 +37,11 @@ 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.downloadFile import com.shabinder.common.di.downloadFile
import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.di.utils.ParallelExecutor
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.Status import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import io.github.shabinder.models.formats.Format
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -163,40 +161,19 @@ class ForegroundService : Service(), CoroutineScope {
trackList.forEach { trackList.forEach {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
downloadService.execute { downloadService.execute {
if (!it.videoID.isNullOrBlank()) { // Video ID already known! val url = fetcher.findMp3DownloadLink(it)
downloadTrack(it.videoID!!, it) if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
enqueueDownload(url, it)
} else { } else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
logger.d("Service VideoID") { videoID ?: "Not Found" }
if (videoID.isNullOrBlank()) {
sendTrackBroadcast(Status.FAILED.name, it) sendTrackBroadcast(Status.FAILED.name, it)
failed++ failed++
updateNotification() updateNotification()
allTracksStatus[it.title] = DownloadStatus.Failed allTracksStatus[it.title] = DownloadStatus.Failed
} else { // Found Youtube Video ID
downloadTrack(videoID, it)
} }
} }
} }
} }
} }
}
private suspend fun downloadTrack(videoID: String, track: TrackDetails) {
try {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if (url == null) {
val audioData: Format = ytDownloader?.getVideo(videoID)?.get() ?: throw Exception("Java YT Dependency Error")
val ytUrl = audioData.url!! // We Will catch NPE
enqueueDownload(ytUrl, track)
} else enqueueDownload(url, track)
} catch (e: Exception) {
logger.d("Service YT Error") { e.message.toString() }
sendTrackBroadcast(Status.FAILED.name, track)
allTracksStatus[track.title] = DownloadStatus.Failed
}
}
private suspend fun enqueueDownload(url: String, track: TrackDetails) { private suspend fun enqueueDownload(url: String, track: TrackDetails) {
// Initiating Download // Initiating Download

View File

@ -20,6 +20,7 @@ import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import com.shabinder.common.database.databaseModule import com.shabinder.common.database.databaseModule
import com.shabinder.common.database.getLogger import com.shabinder.common.database.getLogger
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SaavnProvider import com.shabinder.common.di.providers.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.SpotifyProvider
@ -56,13 +57,14 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { Settings() } single { Settings() }
single { Kermit(getLogger()) } single { Kermit(getLogger()) }
single { TokenStore(get(), get()) } single { TokenStore(get(), get()) }
single { YoutubeMusic(get(), get()) } single { AudioToMp3(get(), get()) }
single { SpotifyProvider(get(), get(), get()) } single { SpotifyProvider(get(), get(), get()) }
single { GaanaProvider(get(), get(), get()) } single { GaanaProvider(get(), get(), get()) }
single { SaavnProvider(get(), get(), get()) } single { SaavnProvider(get(), get(), get(), get()) }
single { YoutubeProvider(get(), get(), get()) } single { YoutubeProvider(get(), get(), get()) }
single { YoutubeMp3(get(), get(), get()) } single { YoutubeMp3(get(), get(), get()) }
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get()) } single { YoutubeMusic(get(), get(), get(), get(), get()) }
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get(), get()) }
} }
@ThreadLocal @ThreadLocal

View File

@ -17,23 +17,28 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.database.DownloadRecordDatabaseQueries import com.shabinder.common.database.DownloadRecordDatabaseQueries
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SaavnProvider import com.shabinder.common.di.providers.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic import com.shabinder.common.di.providers.YoutubeMusic
import com.shabinder.common.di.providers.YoutubeProvider import com.shabinder.common.di.providers.YoutubeProvider
import com.shabinder.common.di.providers.get
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 kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class FetchPlatformQueryResult( class FetchPlatformQueryResult(
val gaanaProvider: GaanaProvider, private val gaanaProvider: GaanaProvider,
val spotifyProvider: SpotifyProvider, val spotifyProvider: SpotifyProvider,
val youtubeProvider: YoutubeProvider, val youtubeProvider: YoutubeProvider,
val saavnProvider: SaavnProvider, private val saavnProvider: SaavnProvider,
val youtubeMusic: YoutubeMusic, val youtubeMusic: YoutubeMusic,
val youtubeMp3: YoutubeMp3, val youtubeMp3: YoutubeMp3,
val audioToMp3: AudioToMp3,
val dir: Dir val dir: Dir
) { ) {
private val db: DownloadRecordDatabaseQueries? private val db: DownloadRecordDatabaseQueries?
@ -69,6 +74,40 @@ class FetchPlatformQueryResult(
} }
return result return result
} }
// 1) Try Finding on JioSaavn (better quality upto 320KBPS)
// 2) If Not found try finding on Youtube Music
suspend fun findMp3DownloadLink(
track: TrackDetails
): String? =
if (track.videoID != null) {
// We Already have VideoID
when (track.source) {
Source.JioSaavn -> {
saavnProvider.getSongFromID(track.videoID!!).media_url?.let { m4aLink ->
audioToMp3.convertToMp3(m4aLink)
}
}
Source.YouTube -> {
youtubeMp3.getMp3DownloadLink(track.videoID!!)
?: youtubeProvider.ytDownloader?.getVideo(track.videoID!!)?.get()?.url?.let { m4aLink ->
audioToMp3.convertToMp3(m4aLink)
}
}
else -> {
null/* Do Nothing, We should never reach here for now*/
}
}
} else {
// First Try Getting A Link From JioSaavn
saavnProvider.findSongDownloadURL(
trackName = track.title,
trackArtists = track.artists
)
// Lets Try Fetching Now From Youtube Music
?: youtubeMusic.findSongDownloadURL(track)
}
private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) { private fun addToDatabaseAsync(link: String, result: PlatformQueryResult) {
GlobalScope.launch(dispatcherIO) { GlobalScope.launch(dispatcherIO) {
db?.add( db?.add(

View File

@ -0,0 +1,117 @@
package com.shabinder.common.di.audioToMp3
import co.touchlab.kermit.Kermit
import com.shabinder.common.models.AudioQuality
import io.ktor.client.HttpClient
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.headers
import io.ktor.client.statement.HttpStatement
import io.ktor.http.isSuccess
import kotlinx.coroutines.delay
interface AudioToMp3 {
val client: HttpClient
val logger: Kermit
companion object {
operator fun invoke(
client: HttpClient,
logger: Kermit
): AudioToMp3 {
return object : AudioToMp3 {
override val client: HttpClient = client
override val logger: Kermit = logger
}
}
}
suspend fun convertToMp3(
URL: String,
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): String? {
val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
// (jobStatus.contains("d")) == COMPLETION
var jobStatus: String
var retryCount = 40 // Set it to optimal level
do {
jobStatus = try {
client.get(
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
)
} catch (e: Exception) {
e.printStackTrace()
""
}
retryCount--
logger.i("Job Status") { jobStatus }
if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
} while (!jobStatus.contains("d", true) && retryCount != 0)
return if (jobStatus.equals("d", true)) {
// Return MP3 Download Link
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
} else null
}
/*
* Response Link Ex : `https://www.onlineconverter.com/convert/11affb6d88d31861fe5bcd33da7b10a26c`
* - to start the conversion
* */
private suspend fun convertRequest(
URL: String,
host: String? = null,
audioQuality: AudioQuality = AudioQuality.KBPS160,
): String {
val activeHost = host ?: getHost()
val res = client.submitFormWithBinaryData<String>(
url = activeHost,
formData = formData {
append("class", "audio")
append("from", "audio")
append("to", "mp3")
append("source", "url")
append("url", URL.replace("https:", "http:"))
append("audio_quality", audioQuality.kbps)
}
) {
headers {
header("Host", activeHost.getHostDomain().also { logger.i("AudioToMp3 Host") { it } })
header("Origin", "https://www.onlineconverter.com")
header("Referer", "https://www.onlineconverter.com/")
}
}.run {
logger.d { this }
dropLast(3) // last 3 are useless unicode char
}
val job = client.get<HttpStatement>(res) {
headers {
header("Host", "www.onlineconverter.com")
}
}.execute()
logger.i("Schedule Conversion Job") { job.status.isSuccess().toString() }
return res
}
// Active Host free to process conversion
// ex - https://hostveryfast.onlineconverter.com/file/send
private suspend fun getHost(): String {
return client.get<String>("https://www.onlineconverter.com/get/host") {
headers {
header("Host", "www.onlineconverter.com")
}
}.also { logger.i("Active Host") { it } }
}
// Extract full Domain from URL
// ex - hostveryfast.onlineconverter.com
private fun String.getHostDomain(): String {
return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/")
}
}

View File

@ -2,21 +2,21 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir import com.shabinder.common.di.Dir
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.finalOutputDir import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.saavn.JioSaavnRequests import com.shabinder.common.di.saavn.JioSaavnRequests
import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadStatus 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.TrackDetails
import com.shabinder.common.models.saavn.SaavnSearchResult
import com.shabinder.common.models.saavn.SaavnSong import com.shabinder.common.models.saavn.SaavnSong
import com.shabinder.common.models.spotify.Source import com.shabinder.common.models.spotify.Source
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
class SaavnProvider( class SaavnProvider(
override val httpClient: HttpClient, override val httpClient: HttpClient,
private val logger: Kermit, override val logger: Kermit,
override val audioToMp3: AudioToMp3,
private val dir: Dir, private val dir: Dir,
) : JioSaavnRequests { ) : JioSaavnRequests {
@ -87,66 +87,6 @@ class SaavnProvider(
) )
} }
private fun sortByBestMatch(
tracks: List<SaavnSearchResult>,
trackName: String,
trackArtists: List<String>,
): Map<String, Float> {
/*
* "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in tracks) {
var hasCommonWord = false
val resultName = result.title.toLowerCase().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("Saavn 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 = 0F
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf<String>().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
artistMatchNumber++
}
if (artistMatchNumber == 0F) {
// logger.d{ "Saavn Removing: $result" }
continue
}
val artistMatch: Float = (artistMatchNumber / trackArtists.size.toFloat()) * 100F
val nameMatch: Float = FuzzySearch.partialRatio(resultName, trackName).toFloat() / 100F
val avgMatch = (artistMatch + nameMatch) / 2
linksWithMatchValue[result.id] = avgMatch
}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {
logger.d("Saavn Search") { "Match Found for $trackName - ${!it.isNullOrEmpty()}" }
}
}
private fun SaavnSong.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus { private fun SaavnSong.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
return if (dir.isPresent( return if (dir.isPresent(
dir.finalOutputDir( dir.finalOutputDir(

View File

@ -17,6 +17,7 @@
package com.shabinder.common.di.providers package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit import co.touchlab.kermit.Kermit
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.gaana.corsApi import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack import com.shabinder.common.models.YoutubeTrack
@ -37,9 +38,13 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject import kotlinx.serialization.json.putJsonObject
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
class YoutubeMusic constructor( class YoutubeMusic constructor(
private val logger: Kermit, private val logger: Kermit,
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val youtubeMp3: YoutubeMp3,
private val youtubeProvider: YoutubeProvider,
private val audioToMp3: AudioToMp3
) { ) {
companion object { companion object {
@ -47,10 +52,25 @@ class YoutubeMusic constructor(
const val tag = "YT Music" const val tag = "YT Music"
} }
suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? { suspend fun findSongDownloadURL(
trackDetails: TrackDetails
): String? {
val bestMatchVideoID = getYTIDBestMatch(trackDetails)
return bestMatchVideoID?.let { videoID ->
youtubeMp3.getMp3DownloadLink(videoID) ?: youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url?.let { m4aLink ->
audioToMp3.convertToMp3(
m4aLink
)
}
}
}
suspend fun getYTIDBestMatch(
trackDetails: TrackDetails
): String? {
return try { return try {
sortByBestMatch( sortByBestMatch(
getYTTracks(query), getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}"),
trackName = trackDetails.title, trackName = trackDetails.title,
trackArtists = trackDetails.artists, trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec trackDurationSec = trackDetails.durationSec

View File

@ -1,10 +1,13 @@
package com.shabinder.common.di.saavn package com.shabinder.common.di.saavn
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.globalJson import com.shabinder.common.di.globalJson
import com.shabinder.common.models.saavn.SaavnAlbum import com.shabinder.common.models.saavn.SaavnAlbum
import com.shabinder.common.models.saavn.SaavnPlaylist import com.shabinder.common.models.saavn.SaavnPlaylist
import com.shabinder.common.models.saavn.SaavnSearchResult import com.shabinder.common.models.saavn.SaavnSearchResult
import com.shabinder.common.models.saavn.SaavnSong import com.shabinder.common.models.saavn.SaavnSong
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.github.shabinder.utils.getBoolean import io.github.shabinder.utils.getBoolean
import io.github.shabinder.utils.getJsonArray import io.github.shabinder.utils.getJsonArray
import io.github.shabinder.utils.getJsonObject import io.github.shabinder.utils.getJsonObject
@ -24,11 +27,26 @@ import kotlinx.serialization.json.put
interface JioSaavnRequests { interface JioSaavnRequests {
val audioToMp3: AudioToMp3
val httpClient: HttpClient val httpClient: HttpClient
val logger: Kermit
suspend fun findSongDownloadURL(
trackName: String,
trackArtists: List<String>,
): String? {
val songs = searchForSong(trackName)
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
val m4aLink: String? = bestMatches.keys.firstOrNull()?.let {
getSongFromID(it).media_url
}
val mp3Link = m4aLink?.let { audioToMp3.convertToMp3(it) }
return mp3Link
}
suspend fun searchForSong( suspend fun searchForSong(
query: String, query: String,
includeLyrics: Boolean = true includeLyrics: Boolean = false
): List<SaavnSearchResult> { ): List<SaavnSearchResult> {
/*if (query.startsWith("http") && query.contains("saavn.com")) { /*if (query.startsWith("http") && query.contains("saavn.com")) {
return listOf(getSong(query)) return listOf(getSong(query))
@ -58,6 +76,14 @@ interface JioSaavnRequests {
.formatData(fetchLyrics) .formatData(fetchLyrics)
return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data) return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
} }
suspend fun getSongFromID(
ID: String,
fetchLyrics: Boolean = false
): SaavnSong {
val data = ((globalJson.parseToJsonElement(httpClient.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
.formatData(fetchLyrics)
return globalJson.decodeFromJsonElement(SaavnSong.serializer(), data)
}
private suspend fun getSongID( private suspend fun getSongID(
URL: String, URL: String,
@ -198,6 +224,64 @@ interface JioSaavnRequests {
} }
} }
fun sortByBestMatch(
tracks: List<SaavnSearchResult>,
trackName: String,
trackArtists: List<String>,
): Map<String, Float> {
/*
* "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in tracks) {
var hasCommonWord = false
val resultName = result.title.toLowerCase().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) {
logger.i("Saavn Removing Common Word") { 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
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf<String>().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
artistMatchNumber++
}
if (artistMatchNumber == 0) {
logger.i("Artist Match Saavn Removing") { result.toString() }
continue
}
val artistMatch: Float = (artistMatchNumber.toFloat() / trackArtists.size) * 100
val nameMatch: Float = FuzzySearch.partialRatio(resultName, trackName).toFloat() / 100
val avgMatch = (artistMatch + nameMatch) / 2
linksWithMatchValue[result.id] = avgMatch
}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {
logger.i { "Match Found for $trackName - ${!it.isNullOrEmpty()}" }
}
}
companion object { companion object {
// EndPoints // EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query=" const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="

View File

@ -16,14 +16,11 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.get
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 io.github.shabinder.YoutubeDownloader
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -45,47 +42,11 @@ actual suspend fun downloadTracks(
fetcher: FetchPlatformQueryResult, fetcher: FetchPlatformQueryResult,
dir: Dir dir: Dir
) { ) {
list.forEach { list.forEach { trackDetails ->
DownloadScope.execute { // Send Download to Pool. DownloadScope.execute { // Send Download to Pool.
if (!it.videoID.isNullOrBlank()) { // Video ID already known! val url = fetcher.findMp3DownloadLink(trackDetails)
downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata, fetcher.youtubeMp3) if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
} else { downloadFile(url).collect {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
if (videoId.isNullOrBlank()) {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) }
)
} else { // Found Youtube Video ID
downloadTrack(videoId, it, dir::saveFileWithMetadata, fetcher.youtubeMp3)
}
}
}
}
}
private val ytDownloader = YoutubeDownloader()
suspend fun downloadTrack(
videoID: String,
trackDetails: TrackDetails,
saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (TrackDetails) -> Unit) -> Unit,
youtubeMp3: YoutubeMp3
) {
try {
val link = youtubeMp3.getMp3DownloadLink(videoID) ?: ytDownloader.getVideo(videoID).get()?.url
if (link == null) {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
)
return
}
downloadFile(link).collect {
when (it) { when (it) {
is DownloadResult.Error -> { is DownloadResult.Error -> {
DownloadProgressFlow.emit( DownloadProgressFlow.emit(
@ -102,7 +63,7 @@ suspend fun downloadTrack(
) )
} }
is DownloadResult.Success -> { // Todo clear map is DownloadResult.Success -> { // Todo clear map
saveFileWithMetaData(it.byteArray, trackDetails) {} dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
DownloadProgressFlow.emit( DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0
@ -111,7 +72,13 @@ suspend fun downloadTrack(
} }
} }
} }
} catch (e: java.lang.Exception) { } else {
e.printStackTrace() DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
)
}
}
} }
} }

View File

@ -1,6 +1,5 @@
package com.shabinder.common.di package com.shabinder.common.di
import com.shabinder.common.di.providers.get
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
@ -28,45 +27,9 @@ actual suspend fun downloadTracks(
dir.logger.i { "Downloading ${list.size} Tracks" } dir.logger.i { "Downloading ${list.size} Tracks" }
for (track in list) { for (track in list) {
Downloader.execute { Downloader.execute {
if (!track.videoID.isNullOrBlank()) { // Video ID already known! val url = fetcher.findMp3DownloadLink(track)
dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" } if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata, fetcher) downloadFile(url).collect {
} else {
val searchQuery = "${track.title} - ${track.artists.joinToString(",")}"
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, track)
dir.logger.i { "VideoID: ${track.title} -> $videoId" }
if (videoId.isNullOrBlank()) {
DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
)
} else { // Found Youtube Video ID
downloadTrack(videoId, track, dir::saveFileWithMetadata, fetcher)
}
}
}
}
}
@SharedImmutable
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
suspend fun downloadTrack(
videoID: String,
trackDetails: TrackDetails,
saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails, postProcess: (TrackDetails) -> Unit) -> Unit,
fetcher: FetchPlatformQueryResult
) {
try {
var link = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
fetcher.dir.logger.i { "LINK: $videoID -> $link" }
if (link == null) {
link = fetcher.youtubeProvider.ytDownloader?.getVideo(videoID)?.get()?.url ?: return
}
fetcher.dir.logger.i { "LINK: $videoID -> $link" }
downloadFile(link).collect {
fetcher.dir.logger.d { it.toString() } fetcher.dir.logger.d { it.toString() }
/*Construct a `NEW Map` from frozen Map to Modify for Native Platforms*/ /*Construct a `NEW Map` from frozen Map to Modify for Native Platforms*/
val map: MutableMap<String, DownloadStatus> = when (it) { val map: MutableMap<String, DownloadStatus> = when (it) {
@ -74,22 +37,22 @@ suspend fun downloadTrack(
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0
) { hashMapOf() }.toMutableMap().apply { ) { hashMapOf() }.toMutableMap().apply {
set(trackDetails.title, DownloadStatus.Failed) set(track.title, DownloadStatus.Failed)
} }
} }
is DownloadResult.Progress -> { is DownloadResult.Progress -> {
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0
) { hashMapOf() }.toMutableMap().apply { ) { hashMapOf() }.toMutableMap().apply {
set(trackDetails.title, DownloadStatus.Downloading(it.progress)) set(track.title, DownloadStatus.Downloading(it.progress))
} }
} }
is DownloadResult.Success -> { // Todo clear map is DownloadResult.Success -> { // Todo clear map
saveFileWithMetaData(it.byteArray, trackDetails, methods.value::writeMp3Tags) dir.saveFileWithMetadata(it.byteArray, track, methods.value::writeMp3Tags)
DownloadProgressFlow.replayCache.getOrElse( DownloadProgressFlow.replayCache.getOrElse(
0 0
) { hashMapOf() }.toMutableMap().apply { ) { hashMapOf() }.toMutableMap().apply {
set(trackDetails.title, DownloadStatus.Downloaded) set(track.title, DownloadStatus.Downloaded)
} }
} }
else -> { mutableMapOf() } else -> { mutableMapOf() }
@ -98,7 +61,16 @@ suspend fun downloadTrack(
map as HashMap<String, DownloadStatus> map as HashMap<String, DownloadStatus>
) )
} }
} catch (e: Exception) { } else {
e.printStackTrace() DownloadProgressFlow.emit(
DownloadProgressFlow.replayCache.getOrElse(
0
) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
)
} }
} }
}
}
@SharedImmutable
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)

View File

@ -42,34 +42,11 @@ actual suspend fun downloadTracks(
fetcher: FetchPlatformQueryResult, fetcher: FetchPlatformQueryResult,
dir: Dir dir: Dir
) { ) {
list.forEach { list.forEach { track ->
withContext(dispatcherIO) { withContext(dispatcherIO) {
allTracksStatus[it.title] = DownloadStatus.Queued allTracksStatus[track.title] = DownloadStatus.Queued
if (!it.videoID.isNullOrBlank()) { // Video ID already known! val url = fetcher.findMp3DownloadLink(track)
downloadTrack(it.videoID!!, it, fetcher, dir) if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
} else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
println(videoID + " : " + it.title)
if (videoID.isNullOrBlank()) {
allTracksStatus[it.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
} else { // Found Youtube Video ID
downloadTrack(videoID, it, fetcher, dir)
}
}
DownloadProgressFlow.emit(allTracksStatus)
}
}
}
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPlatformQueryResult, dir: Dir) {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
if (url == null) {
allTracksStatus[track.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
println("No URL to Download")
} else {
downloadFile(url).collect { downloadFile(url).collect {
when (it) { when (it) {
is DownloadResult.Success -> { is DownloadResult.Success -> {
@ -87,5 +64,10 @@ suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPl
} }
DownloadProgressFlow.emit(allTracksStatus) DownloadProgressFlow.emit(allTracksStatus)
} }
} else {
allTracksStatus[track.title] = DownloadStatus.Failed
DownloadProgressFlow.emit(allTracksStatus)
}
}
} }
} }

View File

@ -0,0 +1,25 @@
package audio_conversion
@Suppress("EnumEntryName")
enum class AudioQuality(val kbps: String) {
`128KBPS`("128"),
`160KBPS`("160"),
`192KBPS`("192"),
`224KBPS`("224"),
`256KBPS`("256"),
`320KBPS`("320");
companion object {
fun getQuality(kbps: String): AudioQuality {
return when (kbps) {
"128" -> `128KBPS`
"160" -> `160KBPS`
"192" -> `192KBPS`
"224" -> `224KBPS`
"256" -> `256KBPS`
"320" -> `320KBPS`
else -> `160KBPS`
}
}
}
}

View File

@ -9,35 +9,24 @@ import io.ktor.client.request.headers
import io.ktor.client.statement.HttpStatement import io.ktor.client.statement.HttpStatement
import io.ktor.http.isSuccess import io.ktor.http.isSuccess
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.serialization.json.Json
import utils.debug import utils.debug
interface AudioToMp3 { object AudioToMp3 {
@Suppress("EnumEntryName")
enum class Quality(val kbps: String) {
`128KBPS`("128"),
`160KBPS`("160"),
`192KBPS`("192"),
`224KBPS`("224"),
`256KBPS`("256"),
`320KBPS`("320"),
}
suspend fun convertToMp3( suspend fun convertToMp3(
URL: String, URL: String,
quality: Quality = kotlin.runCatching { Quality.valueOf(URL.substringBeforeLast(".").takeLast(3)) }.getOrNull() ?: Quality.`160KBPS`, audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): String? { ): String? {
val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
val jobLink = convertRequest(URL, activeHost, quality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
// (jobStatus = "d") == COMPLETION // (jobStatus.contains("d")) == COMPLETION
var jobStatus: String var jobStatus: String
var retryCount = 20 var retryCount = 40 // Set it to optimal level
do { do {
jobStatus = try { jobStatus = try {
client.get<String>( client.get(
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}" "${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -46,7 +35,7 @@ interface AudioToMp3 {
} }
retryCount-- retryCount--
debug("Job Status", jobStatus) debug("Job Status", jobStatus)
if (!jobStatus.contains("d")) delay(200) // Add Delay , to give Server Time to process audio if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
} while (!jobStatus.contains("d", true) && retryCount != 0) } while (!jobStatus.contains("d", true) && retryCount != 0)
return if (jobStatus.equals("d", true)) { return if (jobStatus.equals("d", true)) {
@ -62,7 +51,7 @@ interface AudioToMp3 {
private suspend fun convertRequest( private suspend fun convertRequest(
URL: String, URL: String,
host: String? = null, host: String? = null,
quality: Quality = Quality.`320KBPS`, audioQuality: AudioQuality = AudioQuality.`320KBPS`,
): String { ): String {
val activeHost = host ?: getHost() val activeHost = host ?: getHost()
val res = client.submitFormWithBinaryData<String>( val res = client.submitFormWithBinaryData<String>(
@ -73,11 +62,11 @@ interface AudioToMp3 {
append("to", "mp3") append("to", "mp3")
append("source", "url") append("source", "url")
append("url", URL.replace("https:", "http:")) append("url", URL.replace("https:", "http:"))
append("audio_quality", quality.kbps) append("audio_quality", audioQuality.kbps)
} }
) { ) {
headers { headers {
header("Host", activeHost.getHostURL().also { debug(it) }) header("Host", activeHost.getHostDomain().also { debug(it) })
header("Origin", "https://www.onlineconverter.com") header("Origin", "https://www.onlineconverter.com")
header("Referer", "https://www.onlineconverter.com/") header("Referer", "https://www.onlineconverter.com/")
} }
@ -95,6 +84,8 @@ interface AudioToMp3 {
return res return res
} }
// Active Host free to process conversion
// ex - https://hostveryfast.onlineconverter.com/file/send
private suspend fun getHost(): String { private suspend fun getHost(): String {
return client.get<String>("https://www.onlineconverter.com/get/host") { return client.get<String>("https://www.onlineconverter.com/get/host") {
headers { headers {
@ -102,15 +93,9 @@ interface AudioToMp3 {
} }
}.also { debug("Active Host", it) } }.also { debug("Active Host", it) }
} }
// Extract full Domain from URL
private fun String.getHostURL(): String { // ex - hostveryfast.onlineconverter.com
private fun String.getHostDomain(): String {
return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/") return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/")
} }
companion object {
val serializer = Json {
ignoreUnknownKeys = true
isLenient = true
}
}
} }

View File

@ -1,6 +1,8 @@
package jiosaavn package jiosaavn
import analytics_html_img.client import analytics_html_img.client
import audio_conversion.AudioToMp3
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.request.forms.FormDataContent import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.http.Parameters import io.ktor.http.Parameters
@ -16,6 +18,7 @@ import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import utils.debug
val serializer = Json { val serializer = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
@ -24,9 +27,22 @@ val serializer = Json {
interface JioSaavnRequests { interface JioSaavnRequests {
suspend fun findSongDownloadURL(
trackName: String,
trackArtists: List<String>,
): String? {
val songs = searchForSong(trackName)
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
val m4aLink = bestMatches.keys.firstOrNull()?.let {
getSongFromID(it).media_url
}
val mp3Link = m4aLink?.let { AudioToMp3.convertToMp3(it) }
return mp3Link
}
suspend fun searchForSong( suspend fun searchForSong(
query: String, query: String,
includeLyrics: Boolean = true includeLyrics: Boolean = false
): List<SaavnSearchResult> { ): List<SaavnSearchResult> {
/*if (query.startsWith("http") && query.contains("saavn.com")) { /*if (query.startsWith("http") && query.contains("saavn.com")) {
return listOf(getSong(query)) return listOf(getSong(query))
@ -204,6 +220,64 @@ interface JioSaavnRequests {
} }
} }
fun sortByBestMatch(
tracks: List<SaavnSearchResult>,
trackName: String,
trackArtists: List<String>,
): Map<String, Float> {
/*
* "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in tracks) {
var hasCommonWord = false
val resultName = result.title.toLowerCase().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) {
debug("Saavn Removing Common Word: ", 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
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf<String>().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
artistMatchNumber++
}
if (artistMatchNumber == 0) {
debug("Artist Match Saavn Removing: $result")
continue
}
val artistMatch: Float = (artistMatchNumber.toFloat() / trackArtists.size) * 100
val nameMatch: Float = FuzzySearch.partialRatio(resultName, trackName).toFloat() / 100
val avgMatch = (artistMatch + nameMatch) / 2
linksWithMatchValue[result.id] = avgMatch
}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {
debug("Match Found for $trackName - ${!it.isNullOrEmpty()}")
}
}
companion object { companion object {
// EndPoints // EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query=" const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="

View File

@ -1,89 +1,7 @@
package utils package utils
import audio_conversion.AudioToMp3
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import jiosaavn.models.SaavnSearchResult
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
// Test Class- at development Time // Test Class- at development Time
fun main(): Unit = runBlocking { fun main(): Unit = runBlocking {
/*val jioSaavnClient = object : JioSaavnRequests {}
val resp = jioSaavnClient.searchForSong(
query = "Ye Faasla"
)
println(resp.joinToString("\n"))
val matches = sortByBestMatch(
tracks = resp,
trackName = "Ye Faasla",
trackArtists = listOf("Shaan", "Hardy")
)
debug(matches.toString())
val link = matches.keys.firstOrNull()?.let {
jioSaavnClient.getSongFromID(it).media_url
}
debug(link.toString())*/
val link = "https://aac.saavncdn.com/787/956c23404206e8f4822827eff5da61a0_320.mp4"
val audioConverter = object : AudioToMp3 {}
val mp3Link = audioConverter.convertToMp3(link.toString())
debug(mp3Link.toString())
}
private fun sortByBestMatch(
tracks: List<SaavnSearchResult>,
trackName: String,
trackArtists: List<String>,
): Map<String, Float> {
/*
* "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in tracks) {
var hasCommonWord = false
val resultName = result.title.toLowerCase().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) {
debug("Saavn Removing Common Word: ", 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
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf<String>().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
artistMatchNumber++
}
if (artistMatchNumber == 0) {
debug("Artist Match Saavn Removing: $result")
continue
}
val artistMatch: Float = (artistMatchNumber.toFloat() / trackArtists.size) * 100
val nameMatch: Float = FuzzySearch.partialRatio(resultName, trackName).toFloat() / 100
val avgMatch = (artistMatch + nameMatch) / 2
linksWithMatchValue[result.id] = avgMatch
}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {
debug("Match Found for $trackName - ${!it.isNullOrEmpty()}")
}
} }