diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt index b653b41..e26b251 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt @@ -1,38 +1,36 @@ package dev.brahmkshatriya.echo.extension import dev.brahmkshatriya.echo.common.models.Album -import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.common.models.User +import dev.brahmkshatriya.echo.extension.api.DeezerAlbum +import dev.brahmkshatriya.echo.extension.api.DeezerArtist +import dev.brahmkshatriya.echo.extension.api.DeezerLogin +import dev.brahmkshatriya.echo.extension.api.DeezerMedia +import dev.brahmkshatriya.echo.extension.api.DeezerPlaylist +import dev.brahmkshatriya.echo.extension.api.DeezerRadio +import dev.brahmkshatriya.echo.extension.api.DeezerSearch +import dev.brahmkshatriya.echo.extension.api.DeezerTrack +import dev.brahmkshatriya.echo.extension.api.DeezerUtil import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray import kotlinx.serialization.json.putJsonObject import okhttp3.Headers import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody -import java.math.BigInteger import java.net.InetSocketAddress import java.net.Proxy -import java.security.MessageDigest import java.util.Locale -import java.util.UUID import java.util.zip.GZIPInputStream import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory @@ -55,6 +53,11 @@ class DeezerApi(private val session: DeezerSession) { } } + private val json = Json { + isLenient = true + ignoreUnknownKeys = true + } + private val language: String get() = session.settings?.getString("lang") ?: Locale.getDefault().toLanguageTag() @@ -98,13 +101,14 @@ class DeezerApi(private val session: DeezerSession) { originalResponse } } - if (useProxy && session.settings?.getBoolean("proxy") == true) { + if (useProxy && session.settings?.getString("proxy")?.isNotEmpty() == true) { + val proxy = session.settings?.getString("proxy") sslSocketFactory(createTrustAllSslSocketFactory(), createTrustAllTrustManager()) hostnameVerifier { _, _ -> true } proxy( Proxy( Proxy.Type.HTTP, - InetSocketAddress.createUnresolved("uk.proxy.murglar.app", 3128) + InetSocketAddress.createUnresolved(proxy, 3128) ) ) } @@ -130,11 +134,6 @@ class DeezerApi(private val session: DeezerSession) { private val client: OkHttpClient get() = createOkHttpClient(useProxy = true) private val clientNP: OkHttpClient get() = createOkHttpClient(useProxy = false) - private val json = Json { - isLenient = true - ignoreUnknownKeys = true - } - private fun getHeaders(method: String? = ""): Headers { return Headers.Builder().apply { add("Accept", "*/*") @@ -154,7 +153,7 @@ class DeezerApi(private val session: DeezerSession) { }.build() } - private suspend fun callApi(method: String, params: JsonObject = buildJsonObject { }, gatewayInput: String? = ""): String = withContext(Dispatchers.IO) { + suspend fun callApi(method: String, params: JsonObject = buildJsonObject { }, gatewayInput: String? = ""): String = withContext(Dispatchers.IO) { val url = HttpUrl.Builder() .scheme("https") .host("www.deezer.com") @@ -214,366 +213,75 @@ class DeezerApi(private val session: DeezerSession) { responseBody } - suspend fun makeUser(email: String = "", pass: String = ""): List { - val userList = mutableListOf() - val jsonData = callApi("deezer.getUserData") - val jObject = json.decodeFromString(jsonData) - val userResults = jObject["results"]!! - val userObject = userResults.jsonObject["USER"]!! - val token = userResults.jsonObject["checkForm"]!!.jsonPrimitive.content - val userId = userObject.jsonObject["USER_ID"]!!.jsonPrimitive.content - val licenseToken = userObject.jsonObject["OPTIONS"]!!.jsonObject["license_token"]!!.jsonPrimitive.content - val name = userObject.jsonObject["BLOG_NAME"]!!.jsonPrimitive.content - val cover = userObject.jsonObject["USER_PICTURE"]!!.jsonPrimitive.content - val user = User( - id = userId, - name = name, - cover = "https://e-cdns-images.dzcdn.net/images/user/$cover/100x100-000000-80-0-0.jpg".toImageHolder(), - extras = mapOf( - "arl" to arl, - "user_id" to userId, - "sid" to sid, - "token" to token, - "license_token" to licenseToken, - "email" to email, - "pass" to pass - ) - ) - userList.add(user) - return userList - } + //<============= Login =============> - suspend fun getArlByEmail(mail: String, password: String) { - // Get SID - getSid() + private val deezerLogin = DeezerLogin(this, json, session, client) - val clientId = "447462" - val clientSecret = "a83bf7f38ad2f137e444727cfc3775cf" - val md5Password = md5(password) + suspend fun makeUser(email: String = "", pass: String = ""): List = deezerLogin.makeUser(email, pass, arl, sid) - val params = mapOf( - "app_id" to clientId, - "login" to mail, - "password" to md5Password, - "hash" to md5(clientId + mail + md5Password + clientSecret) - ) + suspend fun getArlByEmail(mail: String, password: String) = deezerLogin.getArlByEmail(mail, password, sid) - // Get access token - val responseJson = getToken(params) - val apiResponse = json.decodeFromString(responseJson) - session.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) + fun getSid() = deezerLogin.getSid() - // Get ARL - val arlResponse = callApi("user.getArl") - val arlObject = json.decodeFromString(arlResponse) - session.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) - } + //<============= Media =============> - private fun md5(input: String): String { - val md = MessageDigest.getInstance("MD5") - val digest = md.digest(input.toByteArray()) - return BigInteger(1, digest).toString(16).padStart(32, '0') - } + private val deezerMedia = DeezerMedia(json, clientNP) - private fun getToken(params: Map): String { - val url = "https://connect.deezer.com/oauth/user_auth.php" - val httpUrl = url.toHttpUrlOrNull()!!.newBuilder().apply { - params.forEach { (key, value) -> addQueryParameter(key, value) } - }.build() + fun getMP3MediaUrl(track: Track): JsonObject = deezerMedia.getMP3MediaUrl(track, language, arl, sid, licenseToken) - val request = Request.Builder() - .url(httpUrl) - .get() - .headers( - Headers.headersOf( - "Cookie", "sid=$sid", - "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" - ) - ) - .build() + fun getMediaUrl(track: Track, quality: String): JsonObject = deezerMedia.getMediaUrl(track, quality) - client.newCall(request).execute().use { response -> - if (!response.isSuccessful) throw Exception("Unexpected code $response") - return response.body.string() - } - } + //<============= Search =============> - fun getSid() { - val url = "https://www.deezer.com/" - val request = Request.Builder() - .url(url) - .get() - .build() + private val deezerSearch = DeezerSearch(this, json) - val response = client.newCall(request).execute() - response.headers.forEach { - if (it.second.startsWith("sid=")) { - session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) - } - } - } + suspend fun search(query: String): JsonObject = deezerSearch.search(query) - fun getMP3MediaUrl(track: Track): JsonObject { - val headers = Headers.Builder().apply { - add("Accept-Encoding", "gzip") - add("Accept-Language", language.substringBefore("-")) - add("Cache-Control", "max-age=0") - add("Connection", "Keep-alive") - add("Content-Type", "application/json; charset=utf-8") - add("Cookie", "arl=$arl&sid=$sid") - add("Host", "media.deezer.com") - add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36") - }.build() + suspend fun searchSuggestions(query: String): JsonObject = deezerSearch.searchSuggestions(query) - val url = HttpUrl.Builder() - .scheme("https") - .host("media.deezer.com") - .addPathSegment("v1") - .addPathSegment("get_url") - .build() + suspend fun setSearchHistory(query: String) = deezerSearch.setSearchHistory(query) - val requestBody = json.encodeToString( - buildJsonObject { - put("license_token", licenseToken) - putJsonArray("media") { - add(buildJsonObject { - put("type", "FULL") - putJsonArray("formats") { - add(buildJsonObject { - put("cipher", "BF_CBC_STRIPE") - put("format", "MP3_MISC") - }) - } - }) - } - putJsonArray("track_tokens") { add(track.extras["TRACK_TOKEN"]) } - } - ).toRequestBody("application/json; charset=utf-8".toMediaType()) + suspend fun getSearchHistory(): JsonObject = deezerSearch.getSearchHistory() - val request = Request.Builder() - .url(url) - .post(requestBody) - .headers(headers) - .build() + suspend fun deleteSearchHistory() = deezerSearch.deleteSearchHistory(userId) - val response = clientNP.newCall(request).execute() - val responseBody = response.body.string() + //<============= Tracks =============> - return json.decodeFromString(responseBody) - } + private val deezerTrack = DeezerTrack(this, json) - fun getMediaUrl(track: Track, quality: String): JsonObject { - val url = HttpUrl.Builder() - .scheme("https") - .host("dzmedia.fly.dev") - .addPathSegment("get_url") - .build() + suspend fun track(tracks: Array): JsonObject = deezerTrack.track(tracks) - val formats = when (quality) { - "128" -> arrayOf("MP3_128", "MP3_64", "MP3_MISC") - "flac" -> arrayOf("FLAC", "MP3_320", "MP3_128", "MP3_64", "MP3_MISC") - else -> arrayOf("MP3_320", "MP3_128", "MP3_64", "MP3_MISC") - } + suspend fun getTracks(): JsonObject = deezerTrack.getTracks(userId) - val requestBody = json.encodeToString( - buildJsonObject { - put("formats", buildJsonArray { formats.forEach { add(it) } }) - put ("ids", buildJsonArray{ add(track.id.toLong()) }) - } - ).toRequestBody("application/json; charset=utf-8".toMediaType()) + suspend fun addFavoriteTrack(id: String) = deezerTrack.addFavoriteTrack(id) - val request = Request.Builder() - .url(url) - .post(requestBody) - .build() + suspend fun removeFavoriteTrack(id: String) = deezerTrack.removeFavoriteTrack(id) - val response = clientNP.newCall(request).execute() - val responseBody = response.body.string() + //<============= Artists =============> - return json.decodeFromString(responseBody) - } + private val deezerArtist = DeezerArtist(this, json) - suspend fun search(query: String): JsonObject { - val jsonData = callApi( - method = "deezer.pageSearch", - params = buildJsonObject { - put("nb", 128) - put("query", query) - put("start", 0) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun artist(id: String): JsonObject = deezerArtist.artist(id, language) - suspend fun searchSuggestions(query: String): JsonObject { - val jsonData = callApi( - method = "search_getSuggestedQueries", - params = buildJsonObject { - put("QUERY", query) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun getArtists(): JsonObject = deezerArtist.getArtists(userId) - suspend fun setSearchHistory(query: String) { - callApi( - method = "user.addEntryInSearchHistory", - params = buildJsonObject { - putJsonObject("ENTRY") { - put("query", query) - put("type", "query") - } - } - ) - } + suspend fun followArtist(id: String) = deezerArtist.followArtist(id) - suspend fun getSearchHistory(): JsonObject { - val jsonData = callApi( - method = "deezer.userMenu" - ) - return json.decodeFromString(jsonData) - } - - suspend fun deleteSearchHistory() { - callApi( - method = "user.clearSearchHistory", - params = buildJsonObject { - put("USER_ID", userId) - } - ) - } + suspend fun unfollowArtist(id: String) = deezerArtist.unfollowArtist(id) - suspend fun track(tracks: Array): JsonObject { - val jsonData = callApi( - method = "song.getListData", - params = buildJsonObject { - put("sng_ids", buildJsonArray { tracks.forEach { add(it.id) } }) - } - ) - return json.decodeFromString(jsonData) - } + //<============= Albums =============> - suspend fun getTracks(): JsonObject { - val jsonData = callApi( - method = "favorite_song.getList", - params = buildJsonObject { - put("user_id", userId) - put("tab", "loved") - put("nb", 10000) - put("start", 0) - } - ) - return json.decodeFromString(jsonData) - } + private val deezerAlbum = DeezerAlbum(this, json) - suspend fun addFavoriteTrack(id: String) { - callApi( - method = "favorite_song.add", - params = buildJsonObject { - put("SNG_ID", id) - } - ) - } - - suspend fun removeFavoriteTrack(id: String) { - callApi( - method = "favorite_song.remove", - params = buildJsonObject { - put("SNG_ID", id) - } - ) - } - - suspend fun artist(id: String): JsonObject { - val jsonData = callApi( - method = "deezer.pageArtist", - params = buildJsonObject { - put("art_id", id) - put ("lang", language.substringBefore("-")) - } - ) - return json.decodeFromString(jsonData) - } - - suspend fun getArtists(): JsonObject { - val jsonData = callApi( - method = "deezer.pageProfile", - params = buildJsonObject { - put("nb", 40) - put ("tab", "artists") - put("user_id", userId) - } - ) - return json.decodeFromString(jsonData) - } - - suspend fun followArtist(id: String) { - callApi( - method = "artist.addFavorite", - params = buildJsonObject { - put("ART_ID", id) - putJsonObject("CTXT") { - put("id", id) - put("t", "artist_smartradio") - } - } - ) - } - - suspend fun unfollowArtist(id: String) { - callApi( - method = "artist.deleteFavorite", - params = buildJsonObject { - put("ART_ID", id) - putJsonObject("CTXT") { - put("id", id) - put("t", "artist_smartradio") - } - } - ) - } + suspend fun album(album: Album): JsonObject = deezerAlbum.album(album, language) - suspend fun album(album: Album): JsonObject { - val jsonData = callApi( - method = "deezer.pageAlbum", - params = buildJsonObject { - put("alb_id", album.id) - put("header", true) - put("lang", language.substringBefore("-")) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun getAlbums(): JsonObject = deezerAlbum.getAlbums(userId) - suspend fun getAlbums(): JsonObject { - val jsonData = callApi( - method = "deezer.pageProfile", - params = buildJsonObject { - put("user_id", userId) - put("tab", "albums") - put("nb", 50) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun addFavoriteAlbum(id: String) = deezerAlbum.addFavoriteAlbum(id) - suspend fun addFavoriteAlbum(id: String) { - callApi( - method = "album.addFavorite", - params = buildJsonObject { - put("ALB_ID", id) - } - ) - } + suspend fun removeFavoriteAlbum(id: String) = deezerAlbum.removeFavoriteAlbum(id) - suspend fun removeFavoriteAlbum(id: String) { - callApi( - method = "album.deleteFavorite", - params = buildJsonObject { - put("ALB_ID", id) - } - ) - } + //<============= Shows =============> suspend fun show(album: Album): JsonObject { val jsonData = callApi( @@ -602,164 +310,55 @@ class DeezerApi(private val session: DeezerSession) { return json.decodeFromString(jsonData) } - suspend fun playlist(playlist: Playlist): JsonObject { - val jsonData = callApi( - method = "deezer.pagePlaylist", - params = buildJsonObject { - put("playlist_id", playlist.id) - put ("lang", language.substringBefore("-"), ) - put("nb", playlist.tracks) - put("tags", true) - put("start", 0) - } - ) - return json.decodeFromString(jsonData) - } + //<============= Playlists =============> - suspend fun getPlaylists(): JsonObject { - val jsonData = callApi( - method = "deezer.pageProfile", - params = buildJsonObject { - put("user_id", userId) - put ("tab", "playlists") - put("nb", 100) - } - ) - return json.decodeFromString(jsonData) - } + private val deezerPlaylist = DeezerPlaylist(this, json) - suspend fun addFavoritePlaylist(id: String) { - callApi( - method = "playlist.addFavorite", - params = buildJsonObject { - put("PARENT_PLAYLIST_ID", id) - } - ) - } + suspend fun playlist(playlist: Playlist): JsonObject = deezerPlaylist.playlist(playlist, language) - suspend fun removeFavoritePlaylist(id: String) { - callApi( - method = "playlist.deleteFavorite", - params = buildJsonObject { - put("PLAYLIST_ID", id) - } - ) - } + suspend fun getPlaylists(): JsonObject = deezerPlaylist.getPlaylists(userId) - suspend fun addToPlaylist(playlist: Playlist, tracks: List) { - callApi( - method = "playlist.addSongs", - params = buildJsonObject { - put("playlist_id", playlist.id) - put("songs", buildJsonArray { - tracks.forEach { track -> - add(buildJsonArray { add(track.id); add(0) }) - } - }) - } - ) - } + suspend fun addFavoritePlaylist(id: String) = deezerPlaylist.addFavoritePlaylist(id) - suspend fun removeFromPlaylist(playlist: Playlist, tracks: List, indexes: List) = coroutineScope { - val trackIds = tracks.map { it.id } - val ids = indexes.map { index -> trackIds[index] } + suspend fun removeFavoritePlaylist(id: String) = deezerPlaylist.removeFavoritePlaylist(id) - callApi( - method = "playlist.deleteSongs", - params = buildJsonObject { - put("playlist_id", playlist.id) - put("songs", buildJsonArray { - ids.forEach { id -> - add(buildJsonArray { add(id); add(0) }) - } - }) - } - ) - } + suspend fun addToPlaylist(playlist: Playlist, tracks: List) = deezerPlaylist.addToPlaylist(playlist, tracks) - suspend fun createPlaylist(title: String, description: String? = ""): JsonObject { - val jsonData = callApi( - method = "playlist.create", - params = buildJsonObject { - put("title", title) - put ("description", description, ) - put("songs", buildJsonArray {}) - put("status", 0) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun removeFromPlaylist(playlist: Playlist, tracks: List, indexes: List) = deezerPlaylist.removeFromPlaylist(playlist, tracks, indexes) - suspend fun deletePlaylist(id: String) { - callApi( - method = "playlist.delete", - params = buildJsonObject { - put("playlist_id", id) - } - ) - } + suspend fun createPlaylist(title: String, description: String? = ""): JsonObject = deezerPlaylist.createPlaylist(title,description) - suspend fun updatePlaylist(id: String, title: String, description: String? = "") { - callApi( - method = "playlist.update", - params = buildJsonObject { - put("description", description) - put ("playlist_id", id) - put("status", 0) - put("title", title) - } - ) - } + suspend fun deletePlaylist(id: String) = deezerPlaylist.deletePlaylist(id) - suspend fun updatePlaylistOrder(playlistId: String, ids: MutableList) { - callApi( - method = "playlist.updateOrder", - params = buildJsonObject { - put("order", buildJsonArray { ids.forEach { add(it) } }) - put ("playlist_id", playlistId) - put("position", 0) - } - ) - } + suspend fun updatePlaylist(id: String, title: String, description: String? = "") = deezerPlaylist.updatePlaylist(id, title, description) - suspend fun homePage(): JsonObject { - val jsonData = callApi( - method = "page.get", - gatewayInput = """ - {"PAGE":"home","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"${language.substringBefore("-")}","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} - """.trimIndent() - ) - return json.decodeFromString(jsonData) - } + suspend fun updatePlaylistOrder(playlistId: String, ids: MutableList) = deezerPlaylist.updatePlaylistOrder(playlistId, ids) - suspend fun browsePage(): JsonObject { - val jsonData = callApi( - method = "page.get", - gatewayInput = """ - {"PAGE":"channels/explore/explore-tab","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"message":["call_onboarding"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"${language.substringBefore("-")}","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} - """.trimIndent() - ) - return json.decodeFromString(jsonData) - } + //<============= Radios =============> + + private val deezerRadio = DeezerRadio(this, json) + + suspend fun mix(id: String): JsonObject = deezerRadio.mix(id) + + suspend fun mixArtist(id: String): JsonObject = deezerRadio.mixArtist(id) + + suspend fun radio(trackId: String, artistId: String): JsonObject = deezerRadio.radio(trackId, artistId) - suspend fun channelPage(target: String): JsonObject { + suspend fun flow(id: String): JsonObject = deezerRadio.flow(id, userId) + + //<============= Pages =============> + + suspend fun page(page: String): JsonObject { val jsonData = callApi( method = "page.get", gatewayInput = """ - {"PAGE":"${target.substringAfter("/")}","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"message":["call_onboarding"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"${language.substringBefore("-")}","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} + {"PAGE":"$page","VERSION":"2.5","SUPPORT":{"ads":[],"deeplink-list":["deeplink"],"event-card":["live-event"],"grid-preview-one":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid-preview-two":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"horizontal-list":["track","song"],"item-highlight":["radio"],"large-card":["album","external-link","playlist","show","video-link"],"list":["episode"],"mini-banner":["external-link"],"slideshow":["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],"small-horizontal-grid":["flow"],"long-card-horizontal-grid":["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],"filterable-grid":["flow"]},"LANG":"${language.substringBefore("-")}","OPTIONS":["deeplink_newsandentertainment","deeplink_subscribeoffer"]} """.trimIndent() ) return json.decodeFromString(jsonData) } - suspend fun updateCountry() { - callApi( - method = "user.updateRecommendationCountry", - params = buildJsonObject { - put("RECOMMENDATION_COUNTRY", country) - } - ) - } + //<============= Lyrics =============> fun lyrics(id: String): JsonObject { val request = Request.Builder() @@ -787,116 +386,11 @@ class DeezerApi(private val session: DeezerSession) { return json.decodeFromString(pipeResponse.body.string()) } - suspend fun log(track: Track) = withContext(Dispatchers.IO) { - val id = track.id - val next = track.extras["NEXT"] - val ctxtT: String - val ctxtId = when { - !track.extras["album_id"].isNullOrEmpty() -> { - ctxtT = "album_page" - track.extras["album_id"] - } - !track.extras["playlist_id"].isNullOrEmpty() -> { - ctxtT = "playlist_page" - track.extras["playlist_id"] - } - !track.extras["artist_id"].isNullOrEmpty() -> { - ctxtT = "up_next_artist" - track.extras["artist_id"] - } - !track.extras["user_id"].isNullOrEmpty() -> { - ctxtT = "dynamic_page_user_radio" - userId - } - else -> { - ctxtT = "" - "" - } - } - callApi( - method = "log.listen", - params = buildJsonObject { - putJsonObject("next_media") { - putJsonObject("media") { - put("id", next) - put("type", "song") - } - } - putJsonObject("params") { - putJsonObject("ctxt") { - put("id", ctxtId) - put("t", ctxtT) - } - putJsonObject("dev") { - put("t", 0) - put("v", "10020240822130111") - } - put("is_shuffle", false) - putJsonArray("ls") {} - put("lt", 1) - putJsonObject("media") { - put("format", "MP3_128") - put("id", id) - put("type", "song") - } - putJsonObject("payload") {} - putJsonObject("stat") { - put("pause", 0) - put("seek", 0) - put("sync", 0) - } - put("stream_id", UUID.randomUUID().toString()) - put("timestamp", System.currentTimeMillis() / 1000) - put("ts_listen", System.currentTimeMillis() / 1000) - put("type", 0) - } - } - ) - } + //<============= Util =============> - suspend fun mix(id: String): JsonObject { - val jsonData = callApi( - method = "song.getSearchTrackMix", - params = buildJsonObject { - put("sng_id", id) - put("start_with_input_track", false) - } - ) - return json.decodeFromString(jsonData) - } + private val deezerUtil = DeezerUtil(this) - suspend fun mixArtist(id: String): JsonObject { - val jsonData = callApi( - method = "smart.getSmartRadio", - params = buildJsonObject { - put("art_id", id) - } - ) - return json.decodeFromString(jsonData) - } - - suspend fun radio(trackId: String, artistId: String): JsonObject { - val jsonData = callApi( - method = "radio.getUpNext", - params = buildJsonObject { - put("art_id", artistId) - put("limit", 10) - put("sng_id", trackId) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun updateCountry() = deezerUtil.updateCountry(country) - suspend fun flow(id: String): JsonObject { - val jsonData = callApi( - method = "radio.getUserRadio", - params = buildJsonObject { - if (id != "default") { - put("config_id", id) - } - put("user_id", userId) - } - ) - return json.decodeFromString(jsonData) - } + suspend fun log(track: Track) = deezerUtil.log(track, userId) } \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt index 4c4b15e..a157615 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt @@ -3,7 +3,6 @@ package dev.brahmkshatriya.echo.extension import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.clients.ArtistClient import dev.brahmkshatriya.echo.common.clients.ArtistFollowClient -import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.HomeFeedClient import dev.brahmkshatriya.echo.common.clients.LibraryClient import dev.brahmkshatriya.echo.common.clients.LoginClient @@ -68,11 +67,13 @@ class DeezerExtension : HomeFeedClient, TrackClient, TrackLikeClient, RadioClien override val settingItems: List get() = listOf( - SettingSwitch( + SettingList( "Use Proxy", "proxy", "Use proxy to prevent GEO-Blocking", - false + mutableListOf("No Proxy", "UK", "RU 1", "RU 2"), + mutableListOf("", "uk.proxy.murglar.app", "ru1.proxy.murglar.app", "ru2.proxy.murglar.app"), + 0 ), SettingSwitch( "Enable Logging", @@ -313,7 +314,7 @@ class DeezerExtension : HomeFeedClient, TrackClient, TrackLikeClient, RadioClien } suspend fun channelFeed(target: String): List { - val jsonObject = api.channelPage(target) + val jsonObject = api.page(target.substringAfter("/")) val channelPageResults = jsonObject["results"]!!.jsonObject val channelSections = channelPageResults["sections"]!!.jsonArray return coroutineScope { diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt index 68b978e..3552dc7 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt @@ -5,10 +5,7 @@ import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toMedia import io.ktor.utils.io.ByteChannel import io.ktor.utils.io.writeFully import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import okhttp3.ConnectionPool import okhttp3.OkHttpClient import okhttp3.Protocol diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerAlbum.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerAlbum.kt new file mode 100644 index 0000000..f71b7d4 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerAlbum.kt @@ -0,0 +1,53 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.Album +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class DeezerAlbum(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun album(album: Album, language: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageAlbum", + params = buildJsonObject { + put("alb_id", album.id) + put("header", true) + put("lang", language.substringBefore("-")) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun getAlbums(userId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageProfile", + params = buildJsonObject { + put("user_id", userId) + put("tab", "albums") + put("nb", 50) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun addFavoriteAlbum(id: String) { + deezerApi.callApi( + method = "album.addFavorite", + params = buildJsonObject { + put("ALB_ID", id) + } + ) + } + + suspend fun removeFavoriteAlbum(id: String) { + deezerApi.callApi( + method = "album.deleteFavorite", + params = buildJsonObject { + put("ALB_ID", id) + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerArtist.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerArtist.kt new file mode 100644 index 0000000..78c0cd0 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerArtist.kt @@ -0,0 +1,60 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +class DeezerArtist(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun artist(id: String, language: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageArtist", + params = buildJsonObject { + put("art_id", id) + put ("lang", language.substringBefore("-")) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun getArtists(userId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageProfile", + params = buildJsonObject { + put("nb", 40) + put ("tab", "artists") + put("user_id", userId) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun followArtist(id: String) { + deezerApi.callApi( + method = "artist.addFavorite", + params = buildJsonObject { + put("ART_ID", id) + putJsonObject("CTXT") { + put("id", id) + put("t", "artist_smartradio") + } + } + ) + } + + suspend fun unfollowArtist(id: String) { + deezerApi.callApi( + method = "artist.deleteFavorite", + params = buildJsonObject { + put("ART_ID", id) + putJsonObject("CTXT") { + put("id", id) + put("t", "artist_smartradio") + } + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerLogin.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerLogin.kt new file mode 100644 index 0000000..66bf619 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerLogin.kt @@ -0,0 +1,123 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder +import dev.brahmkshatriya.echo.common.models.User +import dev.brahmkshatriya.echo.extension.DeezerApi +import dev.brahmkshatriya.echo.extension.DeezerSession +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import java.math.BigInteger +import java.security.MessageDigest + +class DeezerLogin( + private val deezerApi: DeezerApi, + private val json: Json, + private val session: DeezerSession, + private val client: OkHttpClient +) { + + suspend fun makeUser(email: String = "", pass: String = "", arl: String, sid: String): List { + val userList = mutableListOf() + val jsonData = deezerApi.callApi("deezer.getUserData") + val jObject = json.decodeFromString(jsonData) + val userResults = jObject["results"]!! + val userObject = userResults.jsonObject["USER"]!! + val token = userResults.jsonObject["checkForm"]!!.jsonPrimitive.content + val userId = userObject.jsonObject["USER_ID"]!!.jsonPrimitive.content + val licenseToken = userObject.jsonObject["OPTIONS"]!!.jsonObject["license_token"]!!.jsonPrimitive.content + val name = userObject.jsonObject["BLOG_NAME"]!!.jsonPrimitive.content + val cover = userObject.jsonObject["USER_PICTURE"]!!.jsonPrimitive.content + val user = User( + id = userId, + name = name, + cover = "https://e-cdns-images.dzcdn.net/images/user/$cover/100x100-000000-80-0-0.jpg".toImageHolder(), + extras = mapOf( + "arl" to arl, + "user_id" to userId, + "sid" to sid, + "token" to token, + "license_token" to licenseToken, + "email" to email, + "pass" to pass + ) + ) + userList.add(user) + return userList + } + + suspend fun getArlByEmail(mail: String, password: String, sid: String) { + // Get SID + getSid() + + val clientId = "447462" + val clientSecret = "a83bf7f38ad2f137e444727cfc3775cf" + val md5Password = md5(password) + + val params = mapOf( + "app_id" to clientId, + "login" to mail, + "password" to md5Password, + "hash" to md5(clientId + mail + md5Password + clientSecret) + ) + + // Get access token + val responseJson = getToken(params, sid) + val apiResponse = json.decodeFromString(responseJson) + session.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) + + // Get ARL + val arlResponse = deezerApi.callApi("user.getArl") + val arlObject = json.decodeFromString(arlResponse) + session.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) + } + + private fun md5(input: String): String { + val md = MessageDigest.getInstance("MD5") + val digest = md.digest(input.toByteArray()) + return BigInteger(1, digest).toString(16).padStart(32, '0') + } + + private fun getToken(params: Map, sid: String): String { + val url = "https://connect.deezer.com/oauth/user_auth.php" + val httpUrl = url.toHttpUrlOrNull()!!.newBuilder().apply { + params.forEach { (key, value) -> addQueryParameter(key, value) } + }.build() + + val request = Request.Builder() + .url(httpUrl) + .get() + .headers( + Headers.headersOf( + "Cookie", "sid=$sid", + "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" + ) + ) + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw Exception("Unexpected code $response") + return response.body.string() + } + } + + fun getSid() { + val url = "https://www.deezer.com/" + val request = Request.Builder() + .url(url) + .get() + .build() + + val response = client.newCall(request).execute() + response.headers.forEach { + if (it.second.startsWith("sid=")) { + session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) + } + } + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerMedia.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerMedia.kt new file mode 100644 index 0000000..c202aee --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerMedia.kt @@ -0,0 +1,100 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.Track +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +class DeezerMedia(private val json: Json, private val clientNP: OkHttpClient) { + + fun getMP3MediaUrl(track: Track, language: String, arl: String, sid: String, licenseToken: String): JsonObject { + val headers = Headers.Builder().apply { + add("Accept-Encoding", "gzip") + add("Accept-Language", language.substringBefore("-")) + add("Cache-Control", "max-age=0") + add("Connection", "Keep-alive") + add("Content-Type", "application/json; charset=utf-8") + add("Cookie", "arl=$arl&sid=$sid") + add("Host", "media.deezer.com") + add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36") + }.build() + + val url = HttpUrl.Builder() + .scheme("https") + .host("media.deezer.com") + .addPathSegment("v1") + .addPathSegment("get_url") + .build() + + val requestBody = json.encodeToString( + buildJsonObject { + put("license_token", licenseToken) + putJsonArray("media") { + add(buildJsonObject { + put("type", "FULL") + putJsonArray("formats") { + add(buildJsonObject { + put("cipher", "BF_CBC_STRIPE") + put("format", "MP3_MISC") + }) + } + }) + } + putJsonArray("track_tokens") { add(track.extras["TRACK_TOKEN"]) } + } + ).toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = Request.Builder() + .url(url) + .post(requestBody) + .headers(headers) + .build() + + val response = clientNP.newCall(request).execute() + val responseBody = response.body.string() + + return json.decodeFromString(responseBody) + } + + fun getMediaUrl(track: Track, quality: String): JsonObject { + val url = HttpUrl.Builder() + .scheme("https") + .host("dzmedia.fly.dev") + .addPathSegment("get_url") + .build() + + val formats = when (quality) { + "128" -> arrayOf("MP3_128", "MP3_64", "MP3_MISC") + "flac" -> arrayOf("FLAC", "MP3_320", "MP3_128", "MP3_64", "MP3_MISC") + else -> arrayOf("MP3_320", "MP3_128", "MP3_64", "MP3_MISC") + } + + val requestBody = json.encodeToString( + buildJsonObject { + put("formats", buildJsonArray { formats.forEach { add(it) } }) + put ("ids", buildJsonArray{ add(track.id.toLong()) }) + } + ).toRequestBody("application/json; charset=utf-8".toMediaType()) + + val request = Request.Builder() + .url(url) + .post(requestBody) + .build() + + val response = clientNP.newCall(request).execute() + val responseBody = response.body.string() + + return json.decodeFromString(responseBody) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerPlaylist.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerPlaylist.kt new file mode 100644 index 0000000..efceb2d --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerPlaylist.kt @@ -0,0 +1,135 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.Playlist +import dev.brahmkshatriya.echo.common.models.Track +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.coroutines.coroutineScope +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class DeezerPlaylist(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun playlist(playlist: Playlist, language: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pagePlaylist", + params = buildJsonObject { + put("playlist_id", playlist.id) + put ("lang", language.substringBefore("-"), ) + put("nb", playlist.tracks) + put("tags", true) + put("start", 0) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun getPlaylists(userId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageProfile", + params = buildJsonObject { + put("user_id", userId) + put ("tab", "playlists") + put("nb", 100) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun addFavoritePlaylist(id: String) { + deezerApi.callApi( + method = "playlist.addFavorite", + params = buildJsonObject { + put("PARENT_PLAYLIST_ID", id) + } + ) + } + + suspend fun removeFavoritePlaylist(id: String) { + deezerApi.callApi( + method = "playlist.deleteFavorite", + params = buildJsonObject { + put("PLAYLIST_ID", id) + } + ) + } + + suspend fun addToPlaylist(playlist: Playlist, tracks: List) { + deezerApi.callApi( + method = "playlist.addSongs", + params = buildJsonObject { + put("playlist_id", playlist.id) + put("songs", buildJsonArray { + tracks.forEach { track -> + add(buildJsonArray { add(track.id); add(0) }) + } + }) + } + ) + } + + suspend fun removeFromPlaylist(playlist: Playlist, tracks: List, indexes: List) = coroutineScope { + val trackIds = tracks.map { it.id } + val ids = indexes.map { index -> trackIds[index] } + + deezerApi.callApi( + method = "playlist.deleteSongs", + params = buildJsonObject { + put("playlist_id", playlist.id) + put("songs", buildJsonArray { + ids.forEach { id -> + add(buildJsonArray { add(id); add(0) }) + } + }) + } + ) + } + + suspend fun createPlaylist(title: String, description: String? = ""): JsonObject { + val jsonData = deezerApi.callApi( + method = "playlist.create", + params = buildJsonObject { + put("title", title) + put ("description", description, ) + put("songs", buildJsonArray {}) + put("status", 0) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun deletePlaylist(id: String) { + deezerApi.callApi( + method = "playlist.delete", + params = buildJsonObject { + put("playlist_id", id) + } + ) + } + + suspend fun updatePlaylist(id: String, title: String, description: String? = "") { + deezerApi.callApi( + method = "playlist.update", + params = buildJsonObject { + put("description", description) + put ("playlist_id", id) + put("status", 0) + put("title", title) + } + ) + } + + suspend fun updatePlaylistOrder(playlistId: String, ids: MutableList) { + deezerApi.callApi( + method = "playlist.updateOrder", + params = buildJsonObject { + put("order", buildJsonArray { ids.forEach { add(it) } }) + put ("playlist_id", playlistId) + put("position", 0) + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerRadio.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerRadio.kt new file mode 100644 index 0000000..4801cd0 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerRadio.kt @@ -0,0 +1,56 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class DeezerRadio(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun mix(id: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "song.getSearchTrackMix", + params = buildJsonObject { + put("sng_id", id) + put("start_with_input_track", false) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun mixArtist(id: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "smart.getSmartRadio", + params = buildJsonObject { + put("art_id", id) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun radio(trackId: String, artistId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "radio.getUpNext", + params = buildJsonObject { + put("art_id", artistId) + put("limit", 10) + put("sng_id", trackId) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun flow(id: String, userId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "radio.getUserRadio", + params = buildJsonObject { + if (id != "default") { + put("config_id", id) + } + put("user_id", userId) + } + ) + return json.decodeFromString(jsonData) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerSearch.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerSearch.kt new file mode 100644 index 0000000..73eba18 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerSearch.kt @@ -0,0 +1,61 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonObject + +class DeezerSearch(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun search(query: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.pageSearch", + params = buildJsonObject { + put("nb", 128) + put("query", query) + put("start", 0) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun searchSuggestions(query: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "search_getSuggestedQueries", + params = buildJsonObject { + put("QUERY", query) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun setSearchHistory(query: String) { + deezerApi.callApi( + method = "user.addEntryInSearchHistory", + params = buildJsonObject { + putJsonObject("ENTRY") { + put("query", query) + put("type", "query") + } + } + ) + } + + suspend fun getSearchHistory(): JsonObject { + val jsonData = deezerApi.callApi( + method = "deezer.userMenu" + ) + return json.decodeFromString(jsonData) + } + + suspend fun deleteSearchHistory(userId: String) { + deezerApi.callApi( + method = "user.clearSearchHistory", + params = buildJsonObject { + put("USER_ID", userId) + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerTrack.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerTrack.kt new file mode 100644 index 0000000..0a14c3f --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerTrack.kt @@ -0,0 +1,54 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.Track +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +class DeezerTrack(private val deezerApi: DeezerApi, private val json: Json) { + + suspend fun track(tracks: Array): JsonObject { + val jsonData = deezerApi.callApi( + method = "song.getListData", + params = buildJsonObject { + put("sng_ids", buildJsonArray { tracks.forEach { add(it.id) } }) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun getTracks(userId: String): JsonObject { + val jsonData = deezerApi.callApi( + method = "favorite_song.getList", + params = buildJsonObject { + put("user_id", userId) + put("tab", "loved") + put("nb", 10000) + put("start", 0) + } + ) + return json.decodeFromString(jsonData) + } + + suspend fun addFavoriteTrack(id: String) { + deezerApi.callApi( + method = "favorite_song.add", + params = buildJsonObject { + put("SNG_ID", id) + } + ) + } + + suspend fun removeFavoriteTrack(id: String) { + deezerApi.callApi( + method = "favorite_song.remove", + params = buildJsonObject { + put("SNG_ID", id) + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerUtil.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerUtil.kt new file mode 100644 index 0000000..4c75f2e --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerUtil.kt @@ -0,0 +1,90 @@ +package dev.brahmkshatriya.echo.extension.api + +import dev.brahmkshatriya.echo.common.models.Track +import dev.brahmkshatriya.echo.extension.DeezerApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import java.util.UUID + +class DeezerUtil(private val deezerApi: DeezerApi) { + + suspend fun updateCountry(country: String) { + deezerApi.callApi( + method = "user.updateRecommendationCountry", + params = buildJsonObject { + put("RECOMMENDATION_COUNTRY", country) + } + ) + } + + suspend fun log(track: Track, userId: String) = withContext(Dispatchers.IO) { + val id = track.id + val next = track.extras["NEXT"] + val ctxtT: String + val ctxtId = when { + !track.extras["album_id"].isNullOrEmpty() -> { + ctxtT = "album_page" + track.extras["album_id"] + } + !track.extras["playlist_id"].isNullOrEmpty() -> { + ctxtT = "playlist_page" + track.extras["playlist_id"] + } + !track.extras["artist_id"].isNullOrEmpty() -> { + ctxtT = "up_next_artist" + track.extras["artist_id"] + } + !track.extras["user_id"].isNullOrEmpty() -> { + ctxtT = "dynamic_page_user_radio" + userId + } + else -> { + ctxtT = "" + "" + } + } + deezerApi.callApi( + method = "log.listen", + params = buildJsonObject { + putJsonObject("next_media") { + putJsonObject("media") { + put("id", next) + put("type", "song") + } + } + putJsonObject("params") { + putJsonObject("ctxt") { + put("id", ctxtId) + put("t", ctxtT) + } + putJsonObject("dev") { + put("t", 0) + put("v", "10020240822130111") + } + put("is_shuffle", false) + putJsonArray("ls") {} + put("lt", 1) + putJsonObject("media") { + put("format", "MP3_128") + put("id", id) + put("type", "song") + } + putJsonObject("payload") {} + putJsonObject("stat") { + put("pause", 0) + put("seek", 0) + put("sync", 0) + } + put("stream_id", UUID.randomUUID().toString()) + put("timestamp", System.currentTimeMillis() / 1000) + put("ts_listen", System.currentTimeMillis() / 1000) + put("type", 0) + } + } + ) + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerHomeFeedClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerHomeFeedClient.kt index 20f4f05..21738c2 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerHomeFeedClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerHomeFeedClient.kt @@ -14,7 +14,7 @@ class DeezerHomeFeedClient(private val api: DeezerApi, private val parser: Deeze fun getHomeFeed(): PagedData = PagedData.Single { DeezerExtension().handleArlExpiration() - val homePageResults = api.homePage()["results"]?.jsonObject + val homePageResults = api.page("home")["results"]?.jsonObject val homeSections = homePageResults?.get("sections")?.jsonArray ?: JsonArray(emptyList()) homeSections.mapNotNull { section -> diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt index c766750..ce8f0af 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt @@ -90,7 +90,7 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea private suspend fun browseFeed(): List { DeezerExtension().handleArlExpiration() api.updateCountry() - val jsonObject = api.browsePage() + val jsonObject = api.page("channels/explore/explore-tab") val browsePageResults = jsonObject["results"]!!.jsonObject val browseSections = browsePageResults["sections"]?.jsonArray ?: JsonArray(emptyList()) return browseSections.mapNotNull { section ->