Saavn Sort By Best Match, Build Fixes

This commit is contained in:
shabinder 2021-05-24 03:49:50 +05:30
parent 5f5473aaf7
commit ac209e8509
8 changed files with 160 additions and 28 deletions

View File

@ -131,7 +131,7 @@ dependencies {
//implementation("com.jakewharton.timber:timber:4.7.1") //implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.6.1") implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("com.github.shabinder:storage-chooser:2.0.4.45") implementation("com.github.shabinder:storage-chooser:2.0.4.45")
implementation("com.google.accompanist:accompanist-insets:0.10.0") implementation("com.google.accompanist:accompanist-insets:0.9.1")
// Test // Test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")

View File

@ -28,7 +28,7 @@ object Versions {
const val ktLint = "10.0.0" const val ktLint = "10.0.0"
// DI // DI
const val koin = "3.0.1" const val koin = "3.0.2"
// Logger // Logger
const val kermit = "0.1.9" const val kermit = "0.1.9"
@ -62,10 +62,10 @@ object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}" val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}" val test = "io.insert-koin:koin-test:${Versions.koin}"
val android = "io.insert-koin:koin-android:${Versions.koin}" val android = "io.insert-koin:koin-android:${Versions.koin}"
val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}" val compose = "io.insert-koin:koin-androidx-compose:3.0.1"
} }
object Androidx { object Androidx {
const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha02" const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha07"
const val core = "androidx.core:core-ktx:1.3.2" const val core = "androidx.core:core-ktx:1.3.2"
const val palette = "androidx.palette:palette-ktx:1.0.0" const val palette = "androidx.palette:palette-ktx:1.0.0"
const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutinesVersion}" const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutinesVersion}"

View File

@ -36,6 +36,7 @@ data class TrackDetails(
var albumArtURL: String, var albumArtURL: String,
var source: Source, var source: Source,
val progress: Int = 2, val progress: Int = 2,
val downloadLink: String? = null,
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded, val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
var outputFilePath: String, // UriString in Android var outputFilePath: String, // UriString in Android
var videoID: String? = null, var videoID: String? = null,

View File

@ -8,8 +8,10 @@ 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(
@ -75,14 +77,76 @@ class SaavnProvider(
year = it.year, year = it.year,
comment = it.copyright_text, comment = it.copyright_text,
trackUrl = it.perma_url, trackUrl = it.perma_url,
videoID = it.id,
downloadLink = it.media_url, // Downloadable Link
downloaded = it.updateStatusIfPresent(type, subFolder), downloaded = it.updateStatusIfPresent(type, subFolder),
albumArtURL = it.image.replace("http:", "https:"), albumArtURL = it.image.replace("http:", "https:"),
lyrics = it.lyrics ?: it.lyrics_snippet, lyrics = it.lyrics ?: it.lyrics_snippet,
videoID = it.media_url, // Downloadable Link
source = Source.JioSaavn, source = Source.JioSaavn,
outputFilePath = dir.finalOutputDir(it.song, type, subFolder, dir.defaultDir(), /*".m4a"*/) outputFilePath = dir.finalOutputDir(it.song, type, subFolder, dir.defaultDir(), /*".m4a"*/)
) )
} }
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

@ -37,14 +37,15 @@ 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
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
class YoutubeMusic constructor( class YoutubeMusic constructor(
private val logger: Kermit, private val logger: Kermit,
private val httpClient: HttpClient, private val httpClient: HttpClient,
) { ) {
private val tag = "YT Music"
companion object {
const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
const val tag = "YT Music"
}
suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? { suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? {
return try { return try {
@ -213,11 +214,11 @@ class YoutubeMusic constructor(
trackName: String, trackName: String,
trackArtists: List<String>, trackArtists: List<String>,
trackDurationSec: Int, trackDurationSec: Int,
): Map<String, Int> { ): Map<String, Float> {
/* /*
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value * "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
**/ **/
val linksWithMatchValue = mutableMapOf<String, Int>() val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in ytTracks) { for (result in ytTracks) {
@ -241,7 +242,7 @@ class YoutubeMusic constructor(
// Find artist match // Find artist match
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up // 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 // match = (no of artist names in result) / (no. of artist names on spotify) * 100
var artistMatchNumber = 0 var artistMatchNumber = 0F
if (result.type == "Song") { if (result.type == "Song") {
for (artist in trackArtists) { for (artist in trackArtists) {
@ -255,26 +256,26 @@ class YoutubeMusic constructor(
} }
} }
if (artistMatchNumber == 0) { if (artistMatchNumber == 0F) {
// logger.d{ "YT Api Removing: $result" } // logger.d{ "YT Api Removing: $result" }
continue continue
} }
val artistMatch = (artistMatchNumber / trackArtists.size) * 100 val artistMatch = (artistMatchNumber / trackArtists.size.toFloat()) * 100F
// Duration Match // Duration Match
/*! time match = 100 - (delta(duration)**2 / original duration * 100) /*! time match = 100 - (delta(duration)**2 / original duration * 100)
! difference in song duration (delta) is usually of the magnitude of a few ! 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 ! seconds, we need to amplify the delta if it is to have any meaningful impact
! wen we calculate the avg match value*/ ! wen we calculate the avg match value*/
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60) val difference: Float = result.duration?.split(":")?.get(0)?.toFloat()?.times(60)
?.plus(result.duration?.split(":")?.get(1)?.toInt() ?: 0) ?.plus(result.duration?.split(":")?.get(1)?.toFloat() ?: 0F)
?.minus(trackDurationSec)?.absoluteValue ?: 0 ?.minus(trackDurationSec)?.absoluteValue ?: 0F
val nonMatchValue: Float = ((difference * difference).toFloat() / trackDurationSec.toFloat()) val nonMatchValue: Float = ((difference * difference) / trackDurationSec.toFloat())
val durationMatch = 100 - (nonMatchValue * 100) val durationMatch: Float = 100 - (nonMatchValue * 100F)
val avgMatch = (artistMatch + durationMatch) / 2 val avgMatch: Float = (artistMatch + durationMatch) / 2F
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt() linksWithMatchValue[result.videoId.toString()] = avgMatch
} }
// logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"} // logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also { return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {

View File

@ -1,6 +1,5 @@
package com.shabinder.common.di.saavn package com.shabinder.common.di.saavn
import android.annotation.SuppressLint
import io.ktor.util.InternalAPI import io.ktor.util.InternalAPI
import io.ktor.util.decodeBase64Bytes import io.ktor.util.decodeBase64Bytes
import java.security.SecureRandom import java.security.SecureRandom
@ -9,7 +8,7 @@ import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory import javax.crypto.SecretKeyFactory
import javax.crypto.spec.DESKeySpec import javax.crypto.spec.DESKeySpec
@SuppressLint("GetInstance") @Suppress("GetInstance")
@OptIn(InternalAPI::class) @OptIn(InternalAPI::class)
actual suspend fun decryptURL(url: String): String { actual suspend fun decryptURL(url: String): String {
val dks = DESKeySpec("38346591".toByteArray()) val dks = DESKeySpec("38346591".toByteArray())

View File

@ -18,6 +18,7 @@ application {
} }
dependencies { dependencies {
implementation(Extras.fuzzyWuzzy)
implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinVersion}") implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinVersion}")
implementation("io.ktor:ktor-client-core:1.5.4") implementation("io.ktor:ktor-client-core:1.5.4")
implementation("io.ktor:ktor-client-apache:1.5.4") implementation("io.ktor:ktor-client-apache:1.5.4")

View File

@ -1,14 +1,80 @@
package utils package utils
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import jiosaavn.JioSaavnRequests import jiosaavn.JioSaavnRequests
import jiosaavn.models.SaavnPlaylist import jiosaavn.models.SaavnSearchResult
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
// Test Class- at development Time // Test Class- at development Time
fun main() = runBlocking { fun main(): Unit = runBlocking {
val jioSaavnClient = object : JioSaavnRequests {} val jioSaavnClient = object : JioSaavnRequests {}
val resp: SaavnPlaylist? = jioSaavnClient.getPlaylist( val resp = jioSaavnClient.searchForSong(
URL = "https://www.jiosaavn.com/featured/hindi_chartbusters/u-75xwHI4ks_" query = "Filhall"
) )
println(resp) println(resp.joinToString("\n"))
val matches = sortByBestMatch(
tracks = resp,
trackName = "Filhall",
trackArtists = listOf("B.Praak", "Nupur Sanon")
)
debug(matches.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()}")
}
} }