diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/MoreInfo.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/MoreInfo.kt new file mode 100644 index 00000000..803fd241 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/MoreInfo.kt @@ -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, +) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnAlbum.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnAlbum.kt new file mode 100644 index 00000000..9729527a --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnAlbum.kt @@ -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, + val title: String, + val year: String +) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnPlaylist.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnPlaylist.kt new file mode 100644 index 00000000..11011ddb --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnPlaylist.kt @@ -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? = 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, + val sub_types: List? = null, + val type: String = "", // chart,etc + val uid: String? = null, +) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSearchResult.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSearchResult.kt new file mode 100644 index 00000000..32dcc9c1 --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSearchResult.kt @@ -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, +) 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 new file mode 100644 index 00000000..d23c391b --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/saavn/SaavnSong.kt @@ -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, + 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 +) diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt index b7e99c6a..fbaa3f7d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/spotify/Source.kt @@ -20,4 +20,5 @@ enum class Source { Spotify, YouTube, Gaana, + JioSaavn } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt new file mode 100644 index 00000000..65d67107 --- /dev/null +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt @@ -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") +} 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 a7289a1a..9401d651 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 @@ -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) { 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 63110d0a..c1bf3e5b 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 @@ -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) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt index b51fa409..476ca9b0 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt @@ -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 } } 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 new file mode 100644 index 00000000..53880006 --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SaavnProvider.kt @@ -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.toTrackDetails(type: String, subFolder: String): List = 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 + } +} diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index 4dcc4f6c..44b67370 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -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() - // 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().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 } } 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 new file mode 100644 index 00000000..578a177e --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnRequests.kt @@ -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 { + /*if (query.startsWith("http") && query.contains("saavn.com")) { + return listOf(getSong(query)) + }*/ + + val searchURL = search_base_url + query + val results = mutableListOf() + (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(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(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(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=" + } +} diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt new file mode 100644 index 00000000..d9e38f2d --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JioSaavnUtils.kt @@ -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("©", "©") +} diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt new file mode 100644 index 00000000..eb845e0e --- /dev/null +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/saavn/JsonUtils.kt @@ -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() +} diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt new file mode 100644 index 00000000..65d67107 --- /dev/null +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt @@ -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") +} diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt new file mode 100644 index 00000000..e5264bbb --- /dev/null +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/saavn/decryptURL.kt @@ -0,0 +1,5 @@ +package com.shabinder.common.di.saavn + +actual suspend fun decryptURL(url: String): String { + TODO("Not yet implemented") +} diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt new file mode 100644 index 00000000..e5264bbb --- /dev/null +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/saavn/decryptURL.kt @@ -0,0 +1,5 @@ +package com.shabinder.common.di.saavn + +actual suspend fun decryptURL(url: String): String { + TODO("Not yet implemented") +} diff --git a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt index cbd9851b..14bb1cb0 100644 --- a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt +++ b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnRequests.kt @@ -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 { + /*if (query.startsWith("http") && query.contains("saavn.com")) { + return listOf(getSong(query)) + }*/ + + val searchURL = search_base_url + query + val results = mutableListOf() + (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(queryURL) { + private suspend fun getSongID( + URL: String, + ): String { + val res = client.get(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(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(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", "") + } + } } } diff --git a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnUtils.kt b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnUtils.kt index 9c53042a..2d203028 100644 --- a/maintenance-tasks/src/main/java/jiosaavn/JioSaavnUtils.kt +++ b/maintenance-tasks/src/main/java/jiosaavn/JioSaavnUtils.kt @@ -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 diff --git a/maintenance-tasks/src/main/java/jiosaavn/models/MoreInfo.kt b/maintenance-tasks/src/main/java/jiosaavn/models/MoreInfo.kt new file mode 100644 index 00000000..db1d70cd --- /dev/null +++ b/maintenance-tasks/src/main/java/jiosaavn/models/MoreInfo.kt @@ -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, +) diff --git a/maintenance-tasks/src/main/java/jiosaavn/models/SaavnAlbum.kt b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnAlbum.kt new file mode 100644 index 00000000..558084d0 --- /dev/null +++ b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnAlbum.kt @@ -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, + val title: String, + val year: String +) diff --git a/maintenance-tasks/src/main/java/jiosaavn/models/SaavnPlaylist.kt b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnPlaylist.kt new file mode 100644 index 00000000..df0bfd9a --- /dev/null +++ b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnPlaylist.kt @@ -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? = 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, + val sub_types: List? = null, + val type: String = "", // chart,etc + val uid: String? = null, +) diff --git a/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSearchResult.kt b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSearchResult.kt new file mode 100644 index 00000000..628018fd --- /dev/null +++ b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSearchResult.kt @@ -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, +) diff --git a/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSong.kt b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSong.kt new file mode 100644 index 00000000..b7c113bf --- /dev/null +++ b/maintenance-tasks/src/main/java/jiosaavn/models/SaavnSong.kt @@ -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, + 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 +) diff --git a/maintenance-tasks/src/main/java/utils/TestClass.kt b/maintenance-tasks/src/main/java/utils/TestClass.kt index acf116a6..dbfe890d 100644 --- a/maintenance-tasks/src/main/java/utils/TestClass.kt +++ b/maintenance-tasks/src/main/java/utils/TestClass.kt @@ -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) }