mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-12-22 20:57:54 +01:00
JIO-Saavn Provider (WIP)
This commit is contained in:
parent
67361b1337
commit
99cce337c6
@ -0,0 +1,10 @@
|
||||
package com.shabinder.common.models.saavn
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MoreInfo(
|
||||
val language: String,
|
||||
val primary_artists: String,
|
||||
val singers: String,
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package com.shabinder.common.models.saavn
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnAlbum(
|
||||
val albumid: String,
|
||||
val image: String,
|
||||
val name: String,
|
||||
val perma_url: String,
|
||||
val primary_artists: String,
|
||||
val primary_artists_id: String,
|
||||
val release_date: String,
|
||||
val songs: List<SaavnSong>,
|
||||
val title: String,
|
||||
val year: String
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
package com.shabinder.common.models.saavn
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnPlaylist(
|
||||
val fan_count: Int? = 0,
|
||||
val firstname: String? = null,
|
||||
val follower_count: Long? = null,
|
||||
val image: String,
|
||||
val images: List<String>? = null,
|
||||
val last_updated: String,
|
||||
val lastname: String? = null,
|
||||
val list_count: String? = null,
|
||||
val listid: String? = null,
|
||||
val listname: String, // Title
|
||||
val perma_url: String,
|
||||
val songs: List<SaavnSong>,
|
||||
val sub_types: List<String>? = null,
|
||||
val type: String = "", // chart,etc
|
||||
val uid: String? = null,
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package com.shabinder.common.models.saavn
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnSearchResult(
|
||||
val album: String? = "",
|
||||
val description: String,
|
||||
val id: String,
|
||||
val image: String,
|
||||
val title: String,
|
||||
val type: String,
|
||||
val url: String,
|
||||
val ctr: Int? = 0,
|
||||
val position: Int? = 0,
|
||||
val more_info: MoreInfo? = null,
|
||||
)
|
@ -0,0 +1,46 @@
|
||||
package com.shabinder.common.models.saavn
|
||||
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
@Serializable
|
||||
data class SaavnSong @OptIn(ExperimentalSerializationApi::class) constructor(
|
||||
@JsonNames("320kbps") val is320kbps: Boolean = false,
|
||||
val album: String,
|
||||
val album_url: String? = null,
|
||||
val albumid: String? = null,
|
||||
val artistMap: Map<String, String>,
|
||||
val copyright_text: String? = null,
|
||||
val duration: String,
|
||||
val encrypted_media_path: String,
|
||||
val encrypted_media_url: String,
|
||||
val explicit_content: Int = 0,
|
||||
val has_lyrics: Boolean = false,
|
||||
val id: String,
|
||||
val image: String,
|
||||
val label: String? = null,
|
||||
val label_url: String? = null,
|
||||
val language: String,
|
||||
val lyrics: String? = null,
|
||||
val lyrics_snippet: String? = null,
|
||||
val media_preview_url: String? = null,
|
||||
val media_url: String? = null, // Downloadable M4A Link
|
||||
val music: String,
|
||||
val music_id: String,
|
||||
val origin: String? = null,
|
||||
val perma_url: String? = null,
|
||||
val play_count: Int = 0,
|
||||
val primary_artists: String,
|
||||
val primary_artists_id: String,
|
||||
val release_date: String, // Format - 2021-05-04
|
||||
val singers: String,
|
||||
val song: String, // title
|
||||
val starring: String? = null,
|
||||
val type: String = "",
|
||||
val vcode: String? = null,
|
||||
val vlink: String? = null,
|
||||
val year: String,
|
||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||
)
|
@ -20,4 +20,5 @@ enum class Source {
|
||||
Spotify,
|
||||
YouTube,
|
||||
Gaana,
|
||||
JioSaavn
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.ktor.util.InternalAPI
|
||||
import io.ktor.util.decodeBase64Bytes
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.DESKeySpec
|
||||
|
||||
@SuppressLint("GetInstance")
|
||||
@OptIn(InternalAPI::class)
|
||||
actual suspend fun decryptURL(url: String): String {
|
||||
val dks = DESKeySpec("38346591".toByteArray())
|
||||
val keyFactory = SecretKeyFactory.getInstance("DES")
|
||||
val key: SecretKey = keyFactory.generateSecret(dks)
|
||||
|
||||
val cipher: Cipher = Cipher.getInstance("DES/ECB/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, key, SecureRandom())
|
||||
}
|
||||
|
||||
return cipher.doFinal(url.decodeBase64Bytes())
|
||||
.decodeToString()
|
||||
.replace("_96.mp4", "_320.mp4")
|
||||
}
|
@ -21,11 +21,13 @@ import com.russhwolf.settings.Settings
|
||||
import com.shabinder.common.database.databaseModule
|
||||
import com.shabinder.common.database.getLogger
|
||||
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 io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.HttpTimeout
|
||||
import io.ktor.client.features.json.JsonFeature
|
||||
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
||||
import io.ktor.client.features.logging.DEFAULT
|
||||
@ -57,9 +59,10 @@ fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||
single { YoutubeMusic(get(), get()) }
|
||||
single { SpotifyProvider(get(), get(), get()) }
|
||||
single { GaanaProvider(get(), get(), get()) }
|
||||
single { SaavnProvider(get(), get(), get()) }
|
||||
single { YoutubeProvider(get(), get(), get()) }
|
||||
single { YoutubeMp3(get(), get(), get()) }
|
||||
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get()) }
|
||||
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
|
||||
@ThreadLocal
|
||||
@ -73,6 +76,7 @@ fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient {
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(globalJson)
|
||||
}
|
||||
install(HttpTimeout)
|
||||
// WorkAround for Freezing
|
||||
// Use httpClient.getData / httpClient.postData Extensions
|
||||
/*install(JsonFeature) {
|
||||
|
@ -18,6 +18,7 @@ package com.shabinder.common.di
|
||||
|
||||
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||
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
|
||||
@ -30,6 +31,7 @@ class FetchPlatformQueryResult(
|
||||
val gaanaProvider: GaanaProvider,
|
||||
val spotifyProvider: SpotifyProvider,
|
||||
val youtubeProvider: YoutubeProvider,
|
||||
val saavnProvider: SaavnProvider,
|
||||
val youtubeMusic: YoutubeMusic,
|
||||
val youtubeMp3: YoutubeMp3,
|
||||
val dir: Dir
|
||||
@ -47,6 +49,10 @@ class FetchPlatformQueryResult(
|
||||
link.contains("youtube.com", true) || link.contains("youtu.be", true) ->
|
||||
youtubeProvider.query(link)
|
||||
|
||||
// Jio Saavn
|
||||
link.contains("saavn", true) ->
|
||||
saavnProvider.query(link)
|
||||
|
||||
// GAANA
|
||||
link.contains("gaana", true) ->
|
||||
gaanaProvider.query(link)
|
||||
|
@ -76,7 +76,6 @@ class GaanaProvider(
|
||||
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
||||
folderType = "Tracks"
|
||||
subFolder = ""
|
||||
it.updateStatusIfPresent(folderType, subFolder)
|
||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||
title = it.track_title
|
||||
coverUrl = it.artworkLink.replace("http:", "https:")
|
||||
@ -86,9 +85,6 @@ class GaanaProvider(
|
||||
getGaanaAlbum(seokey = link).also {
|
||||
folderType = "Albums"
|
||||
subFolder = link
|
||||
it.tracks?.forEach { track ->
|
||||
track.updateStatusIfPresent(folderType, subFolder)
|
||||
}
|
||||
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
|
||||
title = link
|
||||
coverUrl = it.custom_artworks.size_480p.replace("http:", "https:")
|
||||
@ -98,9 +94,6 @@ class GaanaProvider(
|
||||
getGaanaPlaylist(seokey = link).also {
|
||||
folderType = "Playlists"
|
||||
subFolder = link
|
||||
it.tracks.forEach { track ->
|
||||
track.updateStatusIfPresent(folderType, subFolder)
|
||||
}
|
||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||
title = link
|
||||
// coverUrl.value = "TODO"
|
||||
@ -117,9 +110,6 @@ class GaanaProvider(
|
||||
coverUrl = it.artworkLink?.replace("http:", "https:") ?: gaanaPlaceholderImageUrl
|
||||
}
|
||||
getGaanaArtistTracks(seokey = link).also {
|
||||
it.tracks?.forEach { track ->
|
||||
track.updateStatusIfPresent(folderType, subFolder)
|
||||
}
|
||||
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
|
||||
}
|
||||
}
|
||||
@ -141,14 +131,14 @@ class GaanaProvider(
|
||||
year = it.release_date,
|
||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||
trackUrl = it.lyrics_url,
|
||||
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
||||
downloaded = it.updateStatusIfPresent(type, subFolder),
|
||||
source = Source.Gaana,
|
||||
albumArtURL = it.artworkLink.replace("http:", "https:"),
|
||||
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
||||
)
|
||||
}
|
||||
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||
if (dir.isPresent(
|
||||
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
|
||||
return if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
track_title,
|
||||
folderType,
|
||||
@ -157,7 +147,9 @@ class GaanaProvider(
|
||||
)
|
||||
)
|
||||
) { // Download Already Present!!
|
||||
downloaded = DownloadStatus.Downloaded
|
||||
}
|
||||
DownloadStatus.Downloaded.also {
|
||||
downloaded = it
|
||||
}
|
||||
} else downloaded ?: DownloadStatus.NotDownloaded
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
package com.shabinder.common.di.providers
|
||||
|
||||
import co.touchlab.kermit.Kermit
|
||||
import com.shabinder.common.di.Dir
|
||||
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.SaavnSong
|
||||
import com.shabinder.common.models.spotify.Source
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
class SaavnProvider(
|
||||
override val httpClient: HttpClient,
|
||||
private val logger: Kermit,
|
||||
private val dir: Dir,
|
||||
) : JioSaavnRequests {
|
||||
|
||||
suspend fun query(fullLink: String): PlatformQueryResult {
|
||||
val result = PlatformQueryResult(
|
||||
folderType = "",
|
||||
subFolder = "",
|
||||
title = "",
|
||||
coverUrl = "",
|
||||
trackList = listOf(),
|
||||
Source.JioSaavn
|
||||
)
|
||||
with(result) {
|
||||
when (fullLink.substringAfter("saavn.com/").substringBefore("/")) {
|
||||
"song" -> {
|
||||
getSong(fullLink).let {
|
||||
folderType = "Tracks"
|
||||
subFolder = ""
|
||||
trackList = listOf(it).toTrackDetails(folderType, subFolder)
|
||||
title = it.song
|
||||
coverUrl = it.image.replace("http:", "https:")
|
||||
}
|
||||
}
|
||||
"album" -> {
|
||||
getAlbum(fullLink)?.let {
|
||||
folderType = "Albums"
|
||||
subFolder = removeIllegalChars(it.title)
|
||||
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
||||
title = it.title
|
||||
coverUrl = it.image.replace("http:", "https:")
|
||||
}
|
||||
}
|
||||
"featured" -> { // Playlist
|
||||
getPlaylist(fullLink)?.let {
|
||||
folderType = "Playlists"
|
||||
subFolder = removeIllegalChars(it.listname)
|
||||
trackList = it.songs.toTrackDetails(folderType, subFolder)
|
||||
coverUrl = it.image.replace("http:", "https:")
|
||||
title = it.listname
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Handle Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<SaavnSong>.toTrackDetails(type: String, subFolder: String): List<TrackDetails> = this.map {
|
||||
TrackDetails(
|
||||
title = it.song,
|
||||
artists = it.artistMap.keys.toMutableSet().apply { addAll(it.singers.split(",")) }.toList(),
|
||||
durationSec = it.duration.toInt(),
|
||||
albumName = it.album,
|
||||
albumArtPath = dir.imageCacheDir() + (it.image.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||
year = it.year,
|
||||
comment = it.copyright_text,
|
||||
trackUrl = it.perma_url,
|
||||
downloaded = it.updateStatusIfPresent(type, subFolder),
|
||||
albumArtURL = it.image.replace("http:", "https:"),
|
||||
lyrics = it.lyrics ?: it.lyrics_snippet,
|
||||
videoID = it.media_url, // Downloadable Link
|
||||
source = Source.JioSaavn,
|
||||
outputFilePath = dir.finalOutputDir(it.song, type, subFolder, dir.defaultDir(), /*".m4a"*/)
|
||||
)
|
||||
}
|
||||
private fun SaavnSong.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
|
||||
return if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
song,
|
||||
folderType,
|
||||
subFolder,
|
||||
dir.defaultDir()
|
||||
)
|
||||
)
|
||||
) { // Download Already Present!!
|
||||
DownloadStatus.Downloaded.also {
|
||||
downloaded = it
|
||||
}
|
||||
} else downloaded
|
||||
}
|
||||
}
|
@ -24,11 +24,13 @@ import com.shabinder.common.di.finalOutputDir
|
||||
import com.shabinder.common.di.globalJson
|
||||
import com.shabinder.common.di.spotify.SpotifyRequests
|
||||
import com.shabinder.common.di.spotify.authenticateSpotify
|
||||
import com.shabinder.common.models.DownloadStatus
|
||||
import com.shabinder.common.models.NativeAtomicReference
|
||||
import com.shabinder.common.models.PlatformQueryResult
|
||||
import com.shabinder.common.models.TrackDetails
|
||||
import com.shabinder.common.models.spotify.Album
|
||||
import com.shabinder.common.models.spotify.Image
|
||||
import com.shabinder.common.models.spotify.PlaylistTrack
|
||||
import com.shabinder.common.models.spotify.Source
|
||||
import com.shabinder.common.models.spotify.Track
|
||||
import io.ktor.client.HttpClient
|
||||
@ -43,15 +45,6 @@ class SpotifyProvider(
|
||||
private val dir: Dir,
|
||||
) : SpotifyRequests {
|
||||
|
||||
/* init {
|
||||
logger.d { "Creating Spotify Provider" }
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
if (currentPlatform is AllPlatforms.Js) {
|
||||
authenticateSpotifyClient(override = true)
|
||||
} else authenticateSpotifyClient()
|
||||
}
|
||||
}*/
|
||||
|
||||
override suspend fun authenticateSpotifyClient(override: Boolean) {
|
||||
val token = if (override) authenticateSpotify() else tokenStore.getToken()
|
||||
if (token == null) {
|
||||
@ -133,7 +126,6 @@ class SpotifyProvider(
|
||||
getTrack(link).also {
|
||||
folderType = "Tracks"
|
||||
subFolder = ""
|
||||
it.updateStatusIfPresent(folderType, subFolder)
|
||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||
title = it.name.toString()
|
||||
coverUrl = it.album?.images?.elementAtOrNull(0)?.url.toString()
|
||||
@ -145,7 +137,6 @@ class SpotifyProvider(
|
||||
folderType = "Albums"
|
||||
subFolder = albumObject.name.toString()
|
||||
albumObject.tracks?.items?.forEach {
|
||||
it.updateStatusIfPresent(folderType, subFolder)
|
||||
it.album = Album(
|
||||
images = listOf(
|
||||
Image(
|
||||
@ -170,25 +161,25 @@ class SpotifyProvider(
|
||||
val playlistObject = getPlaylist(link)
|
||||
folderType = "Playlists"
|
||||
subFolder = playlistObject.name.toString()
|
||||
val tempTrackList = mutableListOf<Track>()
|
||||
// log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
||||
playlistObject.tracks?.items?.forEach {
|
||||
it.track?.let { it1 ->
|
||||
it1.updateStatusIfPresent(folderType, subFolder)
|
||||
tempTrackList.add(it1)
|
||||
val tempTrackList = mutableListOf<Track>().apply {
|
||||
// Add Fetched Tracks
|
||||
playlistObject.tracks?.items?.mapNotNull(PlaylistTrack::track)?.let {
|
||||
addAll(it)
|
||||
}
|
||||
}
|
||||
var moreTracksAvailable = !playlistObject.tracks?.next.isNullOrBlank()
|
||||
|
||||
// Check For More Tracks If available
|
||||
var moreTracksAvailable = !playlistObject.tracks?.next.isNullOrBlank()
|
||||
while (moreTracksAvailable) {
|
||||
// Check For More Tracks If available
|
||||
// Fetch Remaining Tracks
|
||||
val moreTracks =
|
||||
getPlaylistTracks(link, offset = tempTrackList.size)
|
||||
moreTracks.items?.forEach {
|
||||
it.track?.let { it1 -> tempTrackList.add(it1) }
|
||||
moreTracks.items?.mapNotNull(PlaylistTrack::track)?.let { remTracks ->
|
||||
tempTrackList.addAll(remTracks)
|
||||
}
|
||||
moreTracksAvailable = !moreTracks.next.isNullOrBlank()
|
||||
}
|
||||
|
||||
// log("Total Tracks Fetched", tempTrackList.size.toString())
|
||||
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder)
|
||||
title = playlistObject.name.toString()
|
||||
@ -228,14 +219,14 @@ class SpotifyProvider(
|
||||
year = it.album?.release_date,
|
||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||
trackUrl = it.href,
|
||||
downloaded = it.downloaded,
|
||||
downloaded = it.updateStatusIfPresent(type, subFolder),
|
||||
source = Source.Spotify,
|
||||
albumArtURL = it.album?.images?.firstOrNull()?.url.toString(),
|
||||
outputFilePath = dir.finalOutputDir(it.name.toString(), type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
||||
)
|
||||
}
|
||||
private fun Track.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||
if (dir.isPresent(
|
||||
private fun Track.updateStatusIfPresent(folderType: String, subFolder: String): DownloadStatus {
|
||||
return if (dir.isPresent(
|
||||
dir.finalOutputDir(
|
||||
name.toString(),
|
||||
folderType,
|
||||
@ -244,7 +235,9 @@ class SpotifyProvider(
|
||||
)
|
||||
)
|
||||
) { // Download Already Present!!
|
||||
downloaded = com.shabinder.common.models.DownloadStatus.Downloaded
|
||||
}
|
||||
DownloadStatus.Downloaded.also {
|
||||
downloaded = it
|
||||
}
|
||||
} else downloaded
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,209 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
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.utils.getBoolean
|
||||
import io.github.shabinder.utils.getJsonArray
|
||||
import io.github.shabinder.utils.getJsonObject
|
||||
import io.github.shabinder.utils.getString
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.Parameters
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
interface JioSaavnRequests {
|
||||
|
||||
val httpClient: HttpClient
|
||||
|
||||
suspend fun searchForSong(
|
||||
query: String,
|
||||
includeLyrics: Boolean = true
|
||||
): List<SaavnSearchResult> {
|
||||
/*if (query.startsWith("http") && query.contains("saavn.com")) {
|
||||
return listOf(getSong(query))
|
||||
}*/
|
||||
|
||||
val searchURL = search_base_url + query
|
||||
val results = mutableListOf<SaavnSearchResult>()
|
||||
(globalJson.parseToJsonElement(httpClient.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach {
|
||||
(it as? JsonObject)?.formatData()?.let { jsonObject ->
|
||||
results.add(globalJson.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
suspend fun getLyrics(ID: String): String? {
|
||||
return (Json.parseToJsonElement(httpClient.get(lyrics_base_url + ID)) as JsonObject)
|
||||
.getString("lyrics")
|
||||
}
|
||||
|
||||
suspend fun getSong(
|
||||
URL: String,
|
||||
fetchLyrics: Boolean = false
|
||||
): SaavnSong {
|
||||
val id = getSongID(URL)
|
||||
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,
|
||||
): String {
|
||||
val res = httpClient.get<String>(URL) {
|
||||
body = FormDataContent(
|
||||
Parameters.build {
|
||||
append("bitrate", "320")
|
||||
}
|
||||
)
|
||||
}
|
||||
return try {
|
||||
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"pid\":\"")[1].split("\",\"").first()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPlaylist(
|
||||
URL: String,
|
||||
includeLyrics: Boolean = false
|
||||
): SaavnPlaylist? {
|
||||
return try {
|
||||
globalJson.decodeFromJsonElement(
|
||||
SaavnPlaylist.serializer(),
|
||||
(globalJson.parseToJsonElement(httpClient.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject)
|
||||
.formatData(includeLyrics)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPlaylistID(
|
||||
URL: String
|
||||
): String {
|
||||
val res = httpClient.get<String>(URL)
|
||||
return try {
|
||||
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAlbum(
|
||||
URL: String,
|
||||
includeLyrics: Boolean = false
|
||||
): SaavnAlbum? {
|
||||
return try {
|
||||
globalJson.decodeFromJsonElement(
|
||||
SaavnAlbum.serializer(),
|
||||
(globalJson.parseToJsonElement(httpClient.get(album_details_base_url + getAlbumID(URL))) as JsonObject)
|
||||
.formatData(includeLyrics)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAlbumID(
|
||||
URL: String
|
||||
): String {
|
||||
val res = httpClient.get<String>(URL)
|
||||
return try {
|
||||
res.split("\"album_id\":\"")[1].split('"')[0]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun JsonObject.formatData(
|
||||
includeLyrics: Boolean = false
|
||||
): JsonObject {
|
||||
return buildJsonObject {
|
||||
// Accommodate Incoming Json Object Data
|
||||
// And `Format` everything while iterating
|
||||
this@formatData.forEach {
|
||||
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
|
||||
put(it.key, it.value.jsonPrimitive.content.format())
|
||||
} else {
|
||||
// Format Songs Nested Collection Too
|
||||
if (it.key == "songs" && it.value is JsonArray) {
|
||||
put(
|
||||
it.key,
|
||||
buildJsonArray {
|
||||
getJsonArray("songs")?.forEach { song ->
|
||||
(song as? JsonObject)?.formatData(includeLyrics)?.let { formattedSong ->
|
||||
add(formattedSong)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
put(it.key, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var url = getString("media_preview_url")!!.replace("preview", "aac") // We Will catch NPE
|
||||
url = if (getBoolean("320kbps") == true) {
|
||||
url.replace("_96_p.mp4", "_320.mp4")
|
||||
} else {
|
||||
url.replace("_96_p.mp4", "_160.mp4")
|
||||
}
|
||||
// Add Media URL to JSON Object
|
||||
put("media_url", url)
|
||||
} catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// DECRYPT Encrypted Media URL
|
||||
getString("encrypted_media_url")?.let {
|
||||
put("media_url", decryptURL(it))
|
||||
}
|
||||
// Check if 320 Kbps is available or not
|
||||
if (getBoolean("320kbps") != true && containsKey("media_url")) {
|
||||
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
|
||||
}
|
||||
}
|
||||
// Increase Image Resolution
|
||||
put(
|
||||
"image",
|
||||
getString("image")
|
||||
?.replace("150x150", "500x500")
|
||||
?.replace("50x50", "500x500")
|
||||
)
|
||||
|
||||
// Fetch Lyrics if Requested
|
||||
// Lyrics is HTML Based
|
||||
if (includeLyrics) {
|
||||
if (getBoolean("has_lyrics") == true) {
|
||||
put("lyrics", getString("id")?.let { getLyrics(it) })
|
||||
} else {
|
||||
put("lyrics", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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="
|
||||
const val song_details_base_url = "https://www.jiosaavn.com/api.php?__call=song.getDetails&cc=in&_marker=0%3F_marker%3D0&_format=json&pids="
|
||||
const val album_details_base_url = "https://www.jiosaavn.com/api.php?__call=content.getAlbumDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&albumid="
|
||||
const val playlist_details_base_url = "https://www.jiosaavn.com/api.php?__call=playlist.getDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&listid="
|
||||
const val lyrics_base_url = "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&ctx=web6dot0&api_version=4&_format=json&_marker=0%3F_marker%3D0&lyrics_id="
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
expect suspend fun decryptURL(url: String): String
|
||||
|
||||
internal fun String.format(): String {
|
||||
return this.unescape()
|
||||
.replace(""", "'")
|
||||
.replace("&", "&")
|
||||
.replace("'", "'")
|
||||
.replace("©", "©")
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
/*
|
||||
* JSON UTILS
|
||||
* */
|
||||
fun String.escape(): String {
|
||||
val output = StringBuilder()
|
||||
for (element in this) {
|
||||
val chx = element.toInt()
|
||||
if (chx != 0) {
|
||||
when (element) {
|
||||
'\n' -> {
|
||||
output.append("\\n")
|
||||
}
|
||||
'\t' -> {
|
||||
output.append("\\t")
|
||||
}
|
||||
'\r' -> {
|
||||
output.append("\\r")
|
||||
}
|
||||
'\\' -> {
|
||||
output.append("\\\\")
|
||||
}
|
||||
'"' -> {
|
||||
output.append("\\\"")
|
||||
}
|
||||
'\b' -> {
|
||||
output.append("\\b")
|
||||
}
|
||||
/*chx >= 0x10000 -> {
|
||||
assert(false) { "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't." }
|
||||
}*/
|
||||
/*chx > 127 -> {
|
||||
output.append(String.format("\\u%04x", chx))
|
||||
}*/
|
||||
else -> {
|
||||
output.append(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output.toString()
|
||||
}
|
||||
|
||||
fun String.unescape(): String {
|
||||
val builder = StringBuilder()
|
||||
var i = 0
|
||||
while (i < this.length) {
|
||||
val delimiter = this[i]
|
||||
i++ // consume letter or backslash
|
||||
if (delimiter == '\\' && i < this.length) {
|
||||
|
||||
// consume first after backslash
|
||||
val ch = this[i]
|
||||
i++
|
||||
when (ch) {
|
||||
'\\', '/', '"', '\'' -> {
|
||||
builder.append(ch)
|
||||
}
|
||||
'n' -> builder.append('\n')
|
||||
'r' -> builder.append('\r')
|
||||
't' -> builder.append(
|
||||
'\t'
|
||||
)
|
||||
'b' -> builder.append('\b')
|
||||
'f' -> builder.append("\\f")
|
||||
'u' -> {
|
||||
val hex = StringBuilder()
|
||||
|
||||
// expect 4 digits
|
||||
if (i + 4 > this.length) {
|
||||
throw RuntimeException("Not enough unicode digits! ")
|
||||
}
|
||||
for (x in this.substring(i, i + 4).toCharArray()) {
|
||||
// TODO in 1.5 Kotlin
|
||||
/*if (!x.isLetterOrDigit()) {
|
||||
throw RuntimeException("Bad character in unicode escape.")
|
||||
}*/
|
||||
hex.append(x.toLowerCase())
|
||||
}
|
||||
i += 4 // consume those four digits.
|
||||
val code = hex.toString().toInt(16)
|
||||
builder.append(code.toChar())
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("Illegal escape sequence: \\$ch")
|
||||
}
|
||||
}
|
||||
} else { // it's not a backslash, or it's the last character.
|
||||
builder.append(delimiter)
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.ktor.util.InternalAPI
|
||||
import io.ktor.util.decodeBase64Bytes
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.DESKeySpec
|
||||
|
||||
@SuppressLint("GetInstance")
|
||||
@OptIn(InternalAPI::class)
|
||||
actual suspend fun decryptURL(url: String): String {
|
||||
val dks = DESKeySpec("38346591".toByteArray())
|
||||
val keyFactory = SecretKeyFactory.getInstance("DES")
|
||||
val key: SecretKey = keyFactory.generateSecret(dks)
|
||||
|
||||
val cipher: Cipher = Cipher.getInstance("DES/ECB/PKCS5Padding").apply {
|
||||
init(Cipher.DECRYPT_MODE, key, SecureRandom())
|
||||
}
|
||||
|
||||
return cipher.doFinal(url.decodeBase64Bytes())
|
||||
.decodeToString()
|
||||
.replace("_96.mp4", "_320.mp4")
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
actual suspend fun decryptURL(url: String): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.shabinder.common.di.saavn
|
||||
|
||||
actual suspend fun decryptURL(url: String): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
@ -4,29 +4,63 @@ import analytics_html_img.client
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.Parameters
|
||||
import jiosaavn.models.SaavnAlbum
|
||||
import jiosaavn.models.SaavnPlaylist
|
||||
import jiosaavn.models.SaavnSearchResult
|
||||
import jiosaavn.models.SaavnSong
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
val serializer = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
|
||||
interface JioSaavnRequests {
|
||||
|
||||
fun searchForSong(
|
||||
queryURL: String
|
||||
) {
|
||||
suspend fun searchForSong(
|
||||
query: String,
|
||||
includeLyrics: Boolean = true
|
||||
): List<SaavnSearchResult> {
|
||||
/*if (query.startsWith("http") && query.contains("saavn.com")) {
|
||||
return listOf(getSong(query))
|
||||
}*/
|
||||
|
||||
val searchURL = search_base_url + query
|
||||
val results = mutableListOf<SaavnSearchResult>()
|
||||
(serializer.parseToJsonElement(client.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach {
|
||||
(it as? JsonObject)?.formatData()?.let { jsonObject ->
|
||||
results.add(serializer.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
suspend fun getLyrics(ID: String): String? {
|
||||
return (Json.parseToJsonElement(client.get(lyrics_base_url + ID)) as JsonObject)
|
||||
.getString("lyrics")
|
||||
}
|
||||
|
||||
suspend fun getSong(
|
||||
ID: String,
|
||||
URL: String,
|
||||
fetchLyrics: Boolean = false
|
||||
): JsonObject {
|
||||
return ((Json.parseToJsonElement(client.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
|
||||
): SaavnSong {
|
||||
val id = getSongID(URL)
|
||||
val data = ((serializer.parseToJsonElement(client.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
|
||||
.formatData(fetchLyrics)
|
||||
return serializer.decodeFromJsonElement(SaavnSong.serializer(), data)
|
||||
}
|
||||
|
||||
suspend fun getSongID(
|
||||
queryURL: String,
|
||||
fetchLyrics: Boolean = false
|
||||
): String? {
|
||||
val res = client.get<String>(queryURL) {
|
||||
private suspend fun getSongID(
|
||||
URL: String,
|
||||
): String {
|
||||
val res = client.get<String>(URL) {
|
||||
body = FormDataContent(
|
||||
Parameters.build {
|
||||
append("bitrate", "320")
|
||||
@ -36,7 +70,129 @@ interface JioSaavnRequests {
|
||||
return try {
|
||||
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"pid\":\"").getOrNull(1)?.split("\",\"")?.firstOrNull()
|
||||
res.split("\"pid\":\"")[1].split("\",\"").first()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPlaylist(
|
||||
URL: String,
|
||||
includeLyrics: Boolean = false
|
||||
): SaavnPlaylist? {
|
||||
return try {
|
||||
serializer.decodeFromJsonElement(
|
||||
SaavnPlaylist.serializer(),
|
||||
(serializer.parseToJsonElement(client.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject)
|
||||
.formatData(includeLyrics)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPlaylistID(
|
||||
URL: String
|
||||
): String {
|
||||
val res = client.get<String>(URL)
|
||||
return try {
|
||||
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAlbum(
|
||||
URL: String,
|
||||
includeLyrics: Boolean = false
|
||||
): SaavnAlbum? {
|
||||
return try {
|
||||
serializer.decodeFromJsonElement(
|
||||
SaavnAlbum.serializer(),
|
||||
(serializer.parseToJsonElement(client.get(album_details_base_url + getAlbumID(URL))) as JsonObject)
|
||||
.formatData(includeLyrics)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAlbumID(
|
||||
URL: String
|
||||
): String {
|
||||
val res = client.get<String>(URL)
|
||||
return try {
|
||||
res.split("\"album_id\":\"")[1].split('"')[0]
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
res.split("\"page_id\",\"")[1].split("\",\"")[0]
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun JsonObject.formatData(
|
||||
includeLyrics: Boolean = false
|
||||
): JsonObject {
|
||||
return buildJsonObject {
|
||||
// Accommodate Incoming Json Object Data
|
||||
// And `Format` everything while iterating
|
||||
this@formatData.forEach {
|
||||
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
|
||||
put(it.key, it.value.jsonPrimitive.content.format())
|
||||
} else {
|
||||
// Format Songs Nested Collection Too
|
||||
if (it.key == "songs" && it.value is JsonArray) {
|
||||
put(
|
||||
it.key,
|
||||
buildJsonArray {
|
||||
getJsonArray("songs")?.forEach { song ->
|
||||
(song as? JsonObject)?.formatData(includeLyrics)?.let { formattedSong ->
|
||||
add(formattedSong)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
put(it.key, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var url = getString("media_preview_url")!!.replace("preview", "aac") // We Will catch NPE
|
||||
url = if (getBoolean("320kbps") == true) {
|
||||
url.replace("_96_p.mp4", "_320.mp4")
|
||||
} else {
|
||||
url.replace("_96_p.mp4", "_160.mp4")
|
||||
}
|
||||
// Add Media URL to JSON Object
|
||||
put("media_url", url)
|
||||
} catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// DECRYPT Encrypted Media URL
|
||||
getString("encrypted_media_url")?.let {
|
||||
put("media_url", decryptURL(it))
|
||||
}
|
||||
// Check if 320 Kbps is available or not
|
||||
if (getBoolean("320kbps") != true && containsKey("media_url")) {
|
||||
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
|
||||
}
|
||||
}
|
||||
// Increase Image Resolution
|
||||
put(
|
||||
"image",
|
||||
getString("image")
|
||||
?.replace("150x150", "500x500")
|
||||
?.replace("50x50", "500x500")
|
||||
)
|
||||
|
||||
// Fetch Lyrics if Requested
|
||||
// Lyrics is HTML Based
|
||||
if (includeLyrics) {
|
||||
if (getBoolean("has_lyrics") == true) {
|
||||
put("lyrics", getString("id")?.let { getLyrics(it) })
|
||||
} else {
|
||||
put("lyrics", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import io.ktor.util.decodeBase64Bytes
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@ -17,7 +18,7 @@ import javax.crypto.SecretKey
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.DESKeySpec
|
||||
|
||||
internal fun JsonObject.formatData(
|
||||
internal suspend fun JsonObject.formatData(
|
||||
includeLyrics: Boolean = false
|
||||
): JsonObject {
|
||||
return buildJsonObject {
|
||||
@ -27,7 +28,21 @@ internal fun JsonObject.formatData(
|
||||
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
|
||||
put(it.key, it.value.jsonPrimitive.content.format())
|
||||
} else {
|
||||
put(it.key, it.value)
|
||||
// Format Songs Nested Collection Too
|
||||
if (it.key == "songs" && it.value is JsonArray) {
|
||||
put(
|
||||
it.key,
|
||||
buildJsonArray {
|
||||
getJsonArray("songs")?.forEach { song ->
|
||||
(song as? JsonObject)?.formatData(includeLyrics)?.let { formattedSong ->
|
||||
add(formattedSong)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
put(it.key, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +56,7 @@ internal fun JsonObject.formatData(
|
||||
// Add Media URL to JSON Object
|
||||
put("media_url", url)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// e.printStackTrace()
|
||||
// DECRYPT Encrypted Media URL
|
||||
getString("encrypted_media_url")?.let {
|
||||
put("media_url", decryptURL(it))
|
||||
@ -51,13 +66,29 @@ internal fun JsonObject.formatData(
|
||||
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
|
||||
}
|
||||
}
|
||||
// Increase Image Resolution
|
||||
put(
|
||||
"image",
|
||||
getString("image")
|
||||
?.replace("150x150", "500x500")
|
||||
?.replace("50x50", "500x500")
|
||||
)
|
||||
|
||||
put("image", getString("image")?.replace("150x150", "500x500"))
|
||||
// Fetch Lyrics if Requested
|
||||
// Lyrics is HTML Based
|
||||
if (includeLyrics) {
|
||||
if (getBoolean("has_lyrics") == true) {
|
||||
put("lyrics", getString("id")?.let { object : JioSaavnRequests {}.getLyrics(it) })
|
||||
} else {
|
||||
put("lyrics", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("GetInstance")
|
||||
@OptIn(InternalAPI::class)
|
||||
fun decryptURL(url: String): String {
|
||||
suspend fun decryptURL(url: String): String {
|
||||
val dks = DESKeySpec("38346591".toByteArray())
|
||||
val keyFactory = SecretKeyFactory.getInstance("DES")
|
||||
val key: SecretKey = keyFactory.generateSecret(dks)
|
||||
@ -76,6 +107,7 @@ internal fun String.format(): String {
|
||||
.replace(""", "'")
|
||||
.replace("&", "&")
|
||||
.replace("'", "'")
|
||||
.replace("©", "©")
|
||||
}
|
||||
|
||||
fun JsonObject.getString(key: String): String? = this[key]?.jsonPrimitive?.content
|
||||
|
10
maintenance-tasks/src/main/java/jiosaavn/models/MoreInfo.kt
Normal file
10
maintenance-tasks/src/main/java/jiosaavn/models/MoreInfo.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package jiosaavn.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MoreInfo(
|
||||
val language: String,
|
||||
val primary_artists: String,
|
||||
val singers: String,
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package jiosaavn.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnAlbum(
|
||||
val albumid: String,
|
||||
val image: String,
|
||||
val name: String,
|
||||
val perma_url: String,
|
||||
val primary_artists: String,
|
||||
val primary_artists_id: String,
|
||||
val release_date: String,
|
||||
val songs: List<SaavnSong>,
|
||||
val title: String,
|
||||
val year: String
|
||||
)
|
@ -0,0 +1,22 @@
|
||||
package jiosaavn.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnPlaylist(
|
||||
val fan_count: Int? = 0,
|
||||
val firstname: String? = null,
|
||||
val follower_count: Long? = null,
|
||||
val image: String,
|
||||
val images: List<String>? = null,
|
||||
val last_updated: String,
|
||||
val lastname: String? = null,
|
||||
val list_count: String? = null,
|
||||
val listid: String? = null,
|
||||
val listname: String, // Title
|
||||
val perma_url: String,
|
||||
val songs: List<SaavnSong>,
|
||||
val sub_types: List<String>? = null,
|
||||
val type: String = "", // chart,etc
|
||||
val uid: String? = null,
|
||||
)
|
@ -0,0 +1,17 @@
|
||||
package jiosaavn.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnSearchResult(
|
||||
val album: String? = "",
|
||||
val description: String,
|
||||
val id: String,
|
||||
val image: String,
|
||||
val title: String,
|
||||
val type: String,
|
||||
val url: String,
|
||||
val ctr: Int? = 0,
|
||||
val position: Int? = 0,
|
||||
val more_info: MoreInfo? = null,
|
||||
)
|
41
maintenance-tasks/src/main/java/jiosaavn/models/SaavnSong.kt
Normal file
41
maintenance-tasks/src/main/java/jiosaavn/models/SaavnSong.kt
Normal file
@ -0,0 +1,41 @@
|
||||
package jiosaavn.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaavnSong(
|
||||
val `320kbps`: Boolean,
|
||||
val album: String,
|
||||
val album_url: String? = null,
|
||||
val albumid: String? = null,
|
||||
val artistMap: Map<String, String>,
|
||||
val copyright_text: String? = null,
|
||||
val duration: String,
|
||||
val encrypted_media_path: String,
|
||||
val encrypted_media_url: String,
|
||||
val explicit_content: Int = 0,
|
||||
val has_lyrics: Boolean = false,
|
||||
val id: String,
|
||||
val image: String,
|
||||
val label: String? = null,
|
||||
val label_url: String? = null,
|
||||
val language: String,
|
||||
val lyrics_snippet: String? = null,
|
||||
val media_preview_url: String? = null,
|
||||
val media_url: String? = null, // Downloadable M4A Link
|
||||
val music: String,
|
||||
val music_id: String,
|
||||
val origin: String? = null,
|
||||
val perma_url: String? = null,
|
||||
val play_count: Int = 0,
|
||||
val primary_artists: String,
|
||||
val primary_artists_id: String,
|
||||
val release_date: String, // Format - 2021-05-04
|
||||
val singers: String,
|
||||
val song: String, // title
|
||||
val starring: String? = null,
|
||||
val type: String = "",
|
||||
val vcode: String? = null,
|
||||
val vlink: String? = null,
|
||||
val year: String
|
||||
)
|
@ -1,14 +1,14 @@
|
||||
package utils
|
||||
|
||||
import jiosaavn.JioSaavnRequests
|
||||
import jiosaavn.models.SaavnPlaylist
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
// Test Class- at development Time
|
||||
fun main() = runBlocking {
|
||||
val jioSaavnClient = object : JioSaavnRequests {}
|
||||
val resp = jioSaavnClient.getSongID(
|
||||
queryURL = "https://www.jiosaavn.com/song/nadiyon-paar-let-the-music-play-again-from-roohi/KAM0bj1AAn4"
|
||||
val resp: SaavnPlaylist? = jioSaavnClient.getPlaylist(
|
||||
URL = "https://www.jiosaavn.com/featured/hindi_chartbusters/u-75xwHI4ks_"
|
||||
)
|
||||
|
||||
debug(jioSaavnClient.getSong(resp.toString()).toString())
|
||||
println(resp)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user