diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 89121075..be6184bc 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -71,11 +71,6 @@
-
-
\ No newline at end of file
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AudioQuality.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AudioQuality.kt
new file mode 100644
index 00000000..1d5a7b3b
--- /dev/null
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/AudioQuality.kt
@@ -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
+ }
+ }
+ }
+}
diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt
index d23c391b..fe423e3a 100644
--- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt
+++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.JsonNames
@Serializable
data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
- @JsonNames("320kbps") val is320kbps: Boolean = false,
+ @JsonNames("320kbps") val is320Kbps: Boolean,
val album: String,
val album_url: String? = null,
val albumid: String? = null,
@@ -23,8 +23,8 @@ data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
val label: String? = null,
val label_url: String? = null,
val language: String,
- val lyrics: String? = null,
val lyrics_snippet: String? = null,
+ val lyrics: String? = null,
val media_preview_url: String? = null,
val media_url: String? = null, // Downloadable M4A Link
val music: String,
diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt
index fcede6a6..cbf3079d 100644
--- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt
+++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt
@@ -37,13 +37,11 @@ import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.R
import com.shabinder.common.di.downloadFile
-import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.Status
import com.shabinder.common.models.TrackDetails
-import io.github.shabinder.models.formats.Format
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -163,41 +161,20 @@ class ForegroundService : Service(), CoroutineScope {
trackList.forEach {
launch(Dispatchers.IO) {
downloadService.execute {
- if (!it.videoID.isNullOrBlank()) { // Video ID already known!
- downloadTrack(it.videoID!!, it)
+ val url = fetcher.findMp3DownloadLink(it)
+ if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
+ enqueueDownload(url, it)
} 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)
- failed++
- updateNotification()
- allTracksStatus[it.title] = DownloadStatus.Failed
- } else { // Found Youtube Video ID
- downloadTrack(videoID, it)
- }
+ sendTrackBroadcast(Status.FAILED.name, it)
+ failed++
+ updateNotification()
+ allTracksStatus[it.title] = DownloadStatus.Failed
}
}
}
}
}
- 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) {
// Initiating Download
addToNotification("Downloading ${track.title}")
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
index 9401d651..801a62b8 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt
@@ -20,6 +20,7 @@ import co.touchlab.kermit.Kermit
import com.russhwolf.settings.Settings
import com.shabinder.common.database.databaseModule
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.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider
@@ -56,13 +57,14 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
single { Settings() }
single { Kermit(getLogger()) }
single { TokenStore(get(), get()) }
- single { YoutubeMusic(get(), get()) }
+ single { AudioToMp3(get(), get()) }
single { SpotifyProvider(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 { 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
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
index c1bf3e5b..58a842d9 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt
@@ -17,23 +17,28 @@
package com.shabinder.common.di
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.SaavnProvider
import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.YoutubeMusic
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.TrackDetails
+import com.shabinder.common.models.spotify.Source
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class FetchPlatformQueryResult(
- val gaanaProvider: GaanaProvider,
+ private val gaanaProvider: GaanaProvider,
val spotifyProvider: SpotifyProvider,
val youtubeProvider: YoutubeProvider,
- val saavnProvider: SaavnProvider,
+ private val saavnProvider: SaavnProvider,
val youtubeMusic: YoutubeMusic,
val youtubeMp3: YoutubeMp3,
+ val audioToMp3: AudioToMp3,
val dir: Dir
) {
private val db: DownloadRecordDatabaseQueries?
@@ -69,6 +74,40 @@ class FetchPlatformQueryResult(
}
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) {
GlobalScope.launch(dispatcherIO) {
db?.add(
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt
new file mode 100644
index 00000000..f40d62b6
--- /dev/null
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/audioToMp3/AudioToMp3.kt
@@ -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(
+ 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(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("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("/")
+ }
+}
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
index 75804aa3..290744b7 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt
@@ -2,21 +2,21 @@ package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
import com.shabinder.common.di.Dir
+import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.finalOutputDir
import com.shabinder.common.di.saavn.JioSaavnRequests
import com.shabinder.common.di.utils.removeIllegalChars
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.PlatformQueryResult
import com.shabinder.common.models.TrackDetails
-import com.shabinder.common.models.saavn.SaavnSearchResult
import com.shabinder.common.models.saavn.SaavnSong
import com.shabinder.common.models.spotify.Source
-import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.HttpClient
class SaavnProvider(
override val httpClient: HttpClient,
- private val logger: Kermit,
+ override val logger: Kermit,
+ override val audioToMp3: AudioToMp3,
private val dir: Dir,
) : JioSaavnRequests {
@@ -87,66 +87,6 @@ class SaavnProvider(
)
}
- private fun sortByBestMatch(
- tracks: List,
- trackName: String,
- trackArtists: List,
- ): Map {
-
- /*
- * "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
- **/
- val linksWithMatchValue = mutableMapOf()
-
- 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().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 {
return if (dir.isPresent(
dir.finalOutputDir(
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
index bfa37a57..215a5ae5 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeMusic.kt
@@ -17,6 +17,7 @@
package com.shabinder.common.di.providers
import co.touchlab.kermit.Kermit
+import com.shabinder.common.di.audioToMp3.AudioToMp3
import com.shabinder.common.di.gaana.corsApi
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.YoutubeTrack
@@ -37,9 +38,13 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import kotlin.math.absoluteValue
+
class YoutubeMusic constructor(
private val logger: Kermit,
private val httpClient: HttpClient,
+ private val youtubeMp3: YoutubeMp3,
+ private val youtubeProvider: YoutubeProvider,
+ private val audioToMp3: AudioToMp3
) {
companion object {
@@ -47,10 +52,25 @@ class YoutubeMusic constructor(
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 {
sortByBestMatch(
- getYTTracks(query),
+ getYTTracks("${trackDetails.title} - ${trackDetails.artists.joinToString(",")}"),
trackName = trackDetails.title,
trackArtists = trackDetails.artists,
trackDurationSec = trackDetails.durationSec
diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt
index 578a177e..3e9fc99b 100644
--- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt
+++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt
@@ -1,10 +1,13 @@
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.models.saavn.SaavnAlbum
import com.shabinder.common.models.saavn.SaavnPlaylist
import com.shabinder.common.models.saavn.SaavnSearchResult
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.getJsonArray
import io.github.shabinder.utils.getJsonObject
@@ -24,11 +27,26 @@ import kotlinx.serialization.json.put
interface JioSaavnRequests {
+ val audioToMp3: AudioToMp3
val httpClient: HttpClient
+ val logger: Kermit
+
+ suspend fun findSongDownloadURL(
+ trackName: String,
+ trackArtists: List,
+ ): 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(
query: String,
- includeLyrics: Boolean = true
+ includeLyrics: Boolean = false
): List {
/*if (query.startsWith("http") && query.contains("saavn.com")) {
return listOf(getSong(query))
@@ -58,6 +76,14 @@ interface JioSaavnRequests {
.formatData(fetchLyrics)
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(
URL: String,
@@ -198,6 +224,64 @@ interface JioSaavnRequests {
}
}
+ fun sortByBestMatch(
+ tracks: List,
+ trackName: String,
+ trackArtists: List,
+ ): Map {
+
+ /*
+ * "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
+ **/
+ val linksWithMatchValue = mutableMapOf()
+
+ 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().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 {
// EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="
diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
index 9851ffdd..6bf00c69 100644
--- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
+++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt
@@ -16,14 +16,11 @@
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.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
-import io.github.shabinder.YoutubeDownloader
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -45,73 +42,43 @@ actual suspend fun downloadTracks(
fetcher: FetchPlatformQueryResult,
dir: Dir
) {
- list.forEach {
+ list.forEach { trackDetails ->
DownloadScope.execute { // Send Download to Pool.
- if (!it.videoID.isNullOrBlank()) { // Video ID already known!
- downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata, fetcher.youtubeMp3)
+ val url = fetcher.findMp3DownloadLink(trackDetails)
+ if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
+ downloadFile(url).collect {
+ when (it) {
+ is DownloadResult.Error -> {
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
+ )
+ }
+ is DownloadResult.Progress -> {
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
+ )
+ }
+ is DownloadResult.Success -> { // Todo clear map
+ dir.saveFileWithMetadata(it.byteArray, trackDetails) {}
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
+ )
+ }
+ }
+ }
} else {
- 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)
- }
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
+ )
}
}
}
}
-
-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) {
- is DownloadResult.Error -> {
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
- )
- }
- is DownloadResult.Progress -> {
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
- )
- }
- is DownloadResult.Success -> { // Todo clear map
- saveFileWithMetaData(it.byteArray, trackDetails) {}
- DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
- )
- }
- }
- }
- } catch (e: java.lang.Exception) {
- e.printStackTrace()
- }
-}
diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt
index 706fbab8..396a85ce 100644
--- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt
+++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt
@@ -1,6 +1,5 @@
package com.shabinder.common.di
-import com.shabinder.common.di.providers.get
import com.shabinder.common.di.utils.ParallelExecutor
import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
@@ -28,22 +27,46 @@ actual suspend fun downloadTracks(
dir.logger.i { "Downloading ${list.size} Tracks" }
for (track in list) {
Downloader.execute {
- if (!track.videoID.isNullOrBlank()) { // Video ID already known!
- dir.logger.i { "VideoID: ${track.title} -> ${track.videoID}" }
- downloadTrack(track.videoID!!, track, dir::saveFileWithMetadata, fetcher)
- } 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()) {
+ val url = fetcher.findMp3DownloadLink(track)
+ if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
+ downloadFile(url).collect {
+ fetcher.dir.logger.d { it.toString() }
+ /*Construct a `NEW Map` from frozen Map to Modify for Native Platforms*/
+ val map: MutableMap = when (it) {
+ is DownloadResult.Error -> {
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.toMutableMap().apply {
+ set(track.title, DownloadStatus.Failed)
+ }
+ }
+ is DownloadResult.Progress -> {
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.toMutableMap().apply {
+ set(track.title, DownloadStatus.Downloading(it.progress))
+ }
+ }
+ is DownloadResult.Success -> { // Todo clear map
+ dir.saveFileWithMetadata(it.byteArray, track, methods.value::writeMp3Tags)
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.toMutableMap().apply {
+ set(track.title, DownloadStatus.Downloaded)
+ }
+ }
+ else -> { mutableMapOf() }
+ }
DownloadProgressFlow.emit(
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
+ map as HashMap
)
- } else { // Found Youtube Video ID
- downloadTrack(videoId, track, dir::saveFileWithMetadata, fetcher)
}
+ } else {
+ DownloadProgressFlow.emit(
+ DownloadProgressFlow.replayCache.getOrElse(
+ 0
+ ) { hashMapOf() }.apply { set(track.title, DownloadStatus.Failed) }
+ )
}
}
}
@@ -51,54 +74,3 @@ actual suspend fun downloadTracks(
@SharedImmutable
val DownloadProgressFlow: MutableSharedFlow> = 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() }
- /*Construct a `NEW Map` from frozen Map to Modify for Native Platforms*/
- val map: MutableMap = when (it) {
- is DownloadResult.Error -> {
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.toMutableMap().apply {
- set(trackDetails.title, DownloadStatus.Failed)
- }
- }
- is DownloadResult.Progress -> {
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.toMutableMap().apply {
- set(trackDetails.title, DownloadStatus.Downloading(it.progress))
- }
- }
- is DownloadResult.Success -> { // Todo clear map
- saveFileWithMetaData(it.byteArray, trackDetails, methods.value::writeMp3Tags)
- DownloadProgressFlow.replayCache.getOrElse(
- 0
- ) { hashMapOf() }.toMutableMap().apply {
- set(trackDetails.title, DownloadStatus.Downloaded)
- }
- }
- else -> { mutableMapOf() }
- }
- DownloadProgressFlow.emit(
- map as HashMap
- )
- }
- } catch (e: Exception) {
- e.printStackTrace()
- }
-}
diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
index f46cb481..7a01defa 100644
--- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
+++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebActual.kt
@@ -42,50 +42,32 @@ actual suspend fun downloadTracks(
fetcher: FetchPlatformQueryResult,
dir: Dir
) {
- list.forEach {
+ list.forEach { track ->
withContext(dispatcherIO) {
- allTracksStatus[it.title] = DownloadStatus.Queued
- if (!it.videoID.isNullOrBlank()) { // Video ID already known!
- downloadTrack(it.videoID!!, it, fetcher, dir)
- } 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
+ allTracksStatus[track.title] = DownloadStatus.Queued
+ val url = fetcher.findMp3DownloadLink(track)
+ if (!url.isNullOrBlank()) { // Successfully Grabbed Mp3 URL
+ downloadFile(url).collect {
+ when (it) {
+ is DownloadResult.Success -> {
+ println("Download Completed")
+ dir.saveFileWithMetadata(it.byteArray, track) {}
+ }
+ is DownloadResult.Error -> {
+ allTracksStatus[track.title] = DownloadStatus.Failed
+ println("Download Error: ${track.title}")
+ }
+ is DownloadResult.Progress -> {
+ allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
+ println("Download Progress: ${it.progress} : ${track.title}")
+ }
+ }
DownloadProgressFlow.emit(allTracksStatus)
- } else { // Found Youtube Video ID
- downloadTrack(videoID, it, fetcher, dir)
}
+ } else {
+ allTracksStatus[track.title] = DownloadStatus.Failed
+ DownloadProgressFlow.emit(allTracksStatus)
}
- 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 {
- when (it) {
- is DownloadResult.Success -> {
- println("Download Completed")
- dir.saveFileWithMetadata(it.byteArray, track) {}
- }
- is DownloadResult.Error -> {
- allTracksStatus[track.title] = DownloadStatus.Failed
- println("Download Error: ${track.title}")
- }
- is DownloadResult.Progress -> {
- allTracksStatus[track.title] = DownloadStatus.Downloading(it.progress)
- println("Download Progress: ${it.progress} : ${track.title}")
- }
- }
- DownloadProgressFlow.emit(allTracksStatus)
}
}
}
diff --git a/maintenance-tasks/src/main/java/audio_conversion/AudioQuality.kt b/maintenance-tasks/src/main/java/audio_conversion/AudioQuality.kt
new file mode 100644
index 00000000..0a47ee77
--- /dev/null
+++ b/maintenance-tasks/src/main/java/audio_conversion/AudioQuality.kt
@@ -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`
+ }
+ }
+ }
+}
diff --git a/maintenance-tasks/src/main/java/audio_conversion/AudioToMp3.kt b/maintenance-tasks/src/main/java/audio_conversion/AudioToMp3.kt
index c4632e8a..05ee8d6d 100644
--- a/maintenance-tasks/src/main/java/audio_conversion/AudioToMp3.kt
+++ b/maintenance-tasks/src/main/java/audio_conversion/AudioToMp3.kt
@@ -9,35 +9,24 @@ import io.ktor.client.request.headers
import io.ktor.client.statement.HttpStatement
import io.ktor.http.isSuccess
import kotlinx.coroutines.delay
-import kotlinx.serialization.json.Json
import utils.debug
-interface AudioToMp3 {
-
- @Suppress("EnumEntryName")
- enum class Quality(val kbps: String) {
- `128KBPS`("128"),
- `160KBPS`("160"),
- `192KBPS`("192"),
- `224KBPS`("224"),
- `256KBPS`("256"),
- `320KBPS`("320"),
- }
+object AudioToMp3 {
suspend fun convertToMp3(
URL: String,
- quality: Quality = kotlin.runCatching { Quality.valueOf(URL.substringBeforeLast(".").takeLast(3)) }.getOrNull() ?: Quality.`160KBPS`,
+ audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): String? {
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 retryCount = 20
+ var retryCount = 40 // Set it to optimal level
do {
jobStatus = try {
- client.get(
+ client.get(
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
)
} catch (e: Exception) {
@@ -46,7 +35,7 @@ interface AudioToMp3 {
}
retryCount--
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)
return if (jobStatus.equals("d", true)) {
@@ -62,7 +51,7 @@ interface AudioToMp3 {
private suspend fun convertRequest(
URL: String,
host: String? = null,
- quality: Quality = Quality.`320KBPS`,
+ audioQuality: AudioQuality = AudioQuality.`320KBPS`,
): String {
val activeHost = host ?: getHost()
val res = client.submitFormWithBinaryData(
@@ -73,11 +62,11 @@ interface AudioToMp3 {
append("to", "mp3")
append("source", "url")
append("url", URL.replace("https:", "http:"))
- append("audio_quality", quality.kbps)
+ append("audio_quality", audioQuality.kbps)
}
) {
headers {
- header("Host", activeHost.getHostURL().also { debug(it) })
+ header("Host", activeHost.getHostDomain().also { debug(it) })
header("Origin", "https://www.onlineconverter.com")
header("Referer", "https://www.onlineconverter.com/")
}
@@ -95,6 +84,8 @@ interface AudioToMp3 {
return res
}
+ // Active Host free to process conversion
+ // ex - https://hostveryfast.onlineconverter.com/file/send
private suspend fun getHost(): String {
return client.get("https://www.onlineconverter.com/get/host") {
headers {
@@ -102,15 +93,9 @@ interface AudioToMp3 {
}
}.also { debug("Active Host", it) }
}
-
- private fun String.getHostURL(): String {
+ // Extract full Domain from URL
+ // ex - hostveryfast.onlineconverter.com
+ private fun String.getHostDomain(): String {
return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/")
}
-
- companion object {
- val serializer = Json {
- ignoreUnknownKeys = true
- isLenient = true
- }
- }
}
diff --git a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt
index c48b3528..3c2fcc98 100644
--- a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt
+++ b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt
@@ -1,6 +1,8 @@
package jiosaavn
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.get
import io.ktor.http.Parameters
@@ -16,6 +18,7 @@ import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
+import utils.debug
val serializer = Json {
ignoreUnknownKeys = true
@@ -24,9 +27,22 @@ val serializer = Json {
interface JioSaavnRequests {
+ suspend fun findSongDownloadURL(
+ trackName: String,
+ trackArtists: List,
+ ): 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(
query: String,
- includeLyrics: Boolean = true
+ includeLyrics: Boolean = false
): List {
/*if (query.startsWith("http") && query.contains("saavn.com")) {
return listOf(getSong(query))
@@ -204,6 +220,64 @@ interface JioSaavnRequests {
}
}
+ fun sortByBestMatch(
+ tracks: List,
+ trackName: String,
+ trackArtists: List,
+ ): Map {
+
+ /*
+ * "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
+ **/
+ val linksWithMatchValue = mutableMapOf()
+
+ 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().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 {
// EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="
diff --git a/maintenance-tasks/src/main/java/utils/TestClass.kt b/maintenance-tasks/src/main/java/utils/TestClass.kt
index 67078345..a516fa00 100644
--- a/maintenance-tasks/src/main/java/utils/TestClass.kt
+++ b/maintenance-tasks/src/main/java/utils/TestClass.kt
@@ -1,89 +1,7 @@
package utils
-import audio_conversion.AudioToMp3
-import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
-import jiosaavn.models.SaavnSearchResult
import kotlinx.coroutines.runBlocking
// Test Class- at development Time
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,
- trackName: String,
- trackArtists: List,
-): Map {
-
- /*
- * "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
- **/
- val linksWithMatchValue = mutableMapOf()
-
- 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().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()}")
- }
}