From dc392922e6ab86fc8c94fa2ad9824261ee83ebbf Mon Sep 17 00:00:00 2001 From: LuftVerbot <97435834+LuftVerbot@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:45:45 +0200 Subject: [PATCH] Update to latest echo And also: - Hopefully fix song being stuck after pausing too long - Rework how values are handled in the background - Reorder Artist shelves --- .../echo/extension/Convertors.kt | 253 ----------------- .../echo/extension/DeezerApi.kt | 45 ++- .../echo/extension/DeezerBase.kt | 56 +--- .../echo/extension/DeezerExtension.kt | 78 ++---- .../echo/extension/DeezerParser.kt | 256 ++++++++++++++++++ .../echo/extension/DeezerSession.kt | 59 ++++ .../brahmkshatriya/echo/extension/Utils.kt | 31 ++- .../extension/clients/DeezerAlbumClient.kt | 19 +- .../extension/clients/DeezerArtistClient.kt | 25 +- .../extension/clients/DeezerHomeFeedClient.kt | 15 +- .../extension/clients/DeezerLibraryClient.kt | 14 +- .../extension/clients/DeezerPlaylistClient.kt | 16 +- .../extension/clients/DeezerRadioClient.kt | 12 +- .../extension/clients/DeezerSearchClient.kt | 24 +- .../extension/clients/DeezerTrackClient.kt | 6 +- gradle.properties | 2 +- 16 files changed, 463 insertions(+), 448 deletions(-) delete mode 100644 ext/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt create mode 100644 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerParser.kt create mode 100644 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerSession.kt diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt deleted file mode 100644 index 9633ccc..0000000 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt +++ /dev/null @@ -1,253 +0,0 @@ -package dev.brahmkshatriya.echo.extension - -import dev.brahmkshatriya.echo.common.helpers.PagedData -import dev.brahmkshatriya.echo.common.models.Album -import dev.brahmkshatriya.echo.common.models.Artist -import dev.brahmkshatriya.echo.common.models.EchoMediaItem -import dev.brahmkshatriya.echo.common.models.ImageHolder -import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder -import dev.brahmkshatriya.echo.common.models.Playlist -import dev.brahmkshatriya.echo.common.models.Radio -import dev.brahmkshatriya.echo.common.models.Shelf -import dev.brahmkshatriya.echo.common.models.Streamable -import dev.brahmkshatriya.echo.common.models.Track -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import java.time.Instant -import java.util.Date - -fun JsonElement.toShelfItemsList(name: String = "Unknown"): Shelf? { - val itemsArray = jsonObject["items"]?.jsonArray ?: return null - val list = itemsArray.mapNotNull { it.jsonObject.toEchoMediaItem() } - return if (list.isNotEmpty()) { - Shelf.Lists.Items( - title = name, - list = list - ) - } else { - null - } -} - -fun JsonObject.toShelfItemsList(name: String = "Unknown"): Shelf? { - val item = toEchoMediaItem() ?: return null - return Shelf.Lists.Items( - title = name, - list = listOf(item) - ) -} - -fun JsonArray.toShelfItemsList(name: String = "Unknown"): Shelf? { - val list = mapNotNull { it.jsonObject.toEchoMediaItem() } - return if (list.isNotEmpty()) { - Shelf.Lists.Items( - title = name, - list = list - ) - } else { - null - } -} - -fun JsonElement.toShelfCategoryList(name: String = "Unknown", block: suspend (String) -> List): Shelf.Lists.Categories { - val itemsArray = jsonObject["items"]?.jsonArray ?: return Shelf.Lists.Categories(name, emptyList()) - return Shelf.Lists.Categories( - title = name, - list = itemsArray.take(5).mapNotNull { it.jsonObject.toShelfCategory(block) }, - type = Shelf.Lists.Type.Linear, - more = PagedData.Single { - itemsArray.mapNotNull { it.jsonObject.toShelfCategory(block) } - } - ) -} - -fun JsonObject.toShelfCategory(block: suspend (String) -> List): Shelf.Category? { - val data = this["data"]?.jsonObject ?: this - val type = data["__TYPE__"]?.jsonPrimitive?.content ?: return null - return when { - "channel" in type -> toChannel(block) - else -> null - } -} - -fun JsonObject.toChannel(block: suspend (String) -> List): Shelf.Category { - val data = this["data"]?.jsonObject ?: this - val title = data["title"]?.jsonPrimitive?.content.orEmpty() - val target = this["target"]?.jsonPrimitive?.content.orEmpty() - return Shelf.Category( - title = title, - items = PagedData.Single { - block(target) - }, - ) -} - -fun JsonObject.toEchoMediaItem(): EchoMediaItem? { - val data = this["data"]?.jsonObject ?: this - val type = data["__TYPE__"]?.jsonPrimitive?.content ?: return null - return when { - "playlist" in type -> EchoMediaItem.Lists.PlaylistItem(toPlaylist()) - "album" in type -> EchoMediaItem.Lists.AlbumItem(toAlbum()) - "song" in type -> EchoMediaItem.TrackItem(toTrack()) - "artist" in type -> EchoMediaItem.Profile.ArtistItem(toArtist()) - "show" in type -> EchoMediaItem.Lists.AlbumItem(toShow()) - "episode" in type -> EchoMediaItem.TrackItem(toEpisode()) - "flow" in type -> EchoMediaItem.Lists.RadioItem(toRadio()) - else -> null - } -} - -fun JsonObject.toShow(loaded: Boolean = false): Album { - val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this - val md5 = data["SHOW_ART_MD5"]?.jsonPrimitive?.content.orEmpty() - return Album( - id = data["SHOW_ID"]?.jsonPrimitive?.content.orEmpty(), - title = data["SHOW_NAME"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, "talk", loaded), - tracks = this["EPISODES"]?.jsonObject?.get("total")?.jsonPrimitive?.int, - artists = listOf(Artist(id = "", name = "")), - description = data["SHOW_DESCRIPTION"]?.jsonPrimitive?.content.orEmpty(), - extras = mapOf("__TYPE__" to "show") - ) -} - -fun JsonObject.toEpisode(): Track { - val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this - val md5 = data["SHOW_ART_MD5"]?.jsonPrimitive?.content.orEmpty() - val title = data["EPISODE_TITLE"]?.jsonPrimitive?.content.orEmpty() - return Track( - id = data["EPISODE_ID"]?.jsonPrimitive?.content.orEmpty(), - title = title, - cover = getCover(md5, "talk", false), - duration = data["DURATION"]?.jsonPrimitive?.content?.toLongOrNull()?.times(1000), - streamables = listOf( - Streamable.audio( - id = data["EPISODE_DIRECT_STREAM_URL"]?.jsonPrimitive?.content.orEmpty(), - title = title, - quality = 1 - ) - ), - extras = mapOf( - "TRACK_TOKEN" to data["TRACK_TOKEN"]?.jsonPrimitive?.content.orEmpty(), - "FILESIZE_MP3_MISC" to (data["FILESIZE_MP3_MISC"]?.jsonPrimitive?.content ?: "0"), - "MD5" to md5, - "TYPE" to "talk", - "__TYPE__" to "show" - ) - ) -} - -fun JsonObject.toAlbum(loaded: Boolean = false): Album { - val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this - val md5 = data["ALB_PICTURE"]?.jsonPrimitive?.content.orEmpty() - val artistObject = data["ARTISTS"]?.jsonArray?.firstOrNull()?.jsonObject - val artistMd5 = artistObject?.get("ART_PICTURE")?.jsonPrimitive?.content.orEmpty() - return Album( - id = data["ALB_ID"]?.jsonPrimitive?.content.orEmpty(), - title = data["ALB_TITLE"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, "cover", loaded), - tracks = this["SONGS"]?.jsonObject?.get("total")?.jsonPrimitive?.int, - artists = listOfNotNull( - artistObject?.let { - Artist( - id = it["ART_ID"]?.jsonPrimitive?.content.orEmpty(), - name = it["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(artistMd5, "artist") - ) - } - ), - description = this["description"]?.jsonPrimitive?.content.orEmpty(), - subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty() - ) -} - -fun JsonObject.toArtist(isFollowing: Boolean = false, loaded: Boolean = false): Artist { - val data = this["data"]?.jsonObject ?: this - val md5 = data["ART_PICTURE"]?.jsonPrimitive?.content.orEmpty() - return Artist( - id = data["ART_ID"]?.jsonPrimitive?.content.orEmpty(), - name = data["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, "artist", loaded), - description = this["description"]?.jsonPrimitive?.content.orEmpty(), - subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty(), - isFollowing = isFollowing - ) -} - -@Suppress("NewApi") -fun JsonObject.toTrack(loaded: Boolean = false): Track { - val data = this["data"]?.jsonObject ?: this - val md5 = data["ALB_PICTURE"]?.jsonPrimitive?.content.orEmpty() - val artistObject = data["ARTISTS"]?.jsonArray?.firstOrNull()?.jsonObject ?: data - val artistMd5 = artistObject["ART_PICTURE"]?.jsonPrimitive?.content.orEmpty() - return Track( - id = data["SNG_ID"]?.jsonPrimitive?.content.orEmpty(), - title = data["SNG_TITLE"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, "cover", loaded), - duration = data["DURATION"]?.jsonPrimitive?.content?.toLongOrNull()?.times(1000), - releaseDate = data["DATE_ADD"]?.jsonPrimitive?.content?.toLongOrNull()?.let { - Date.from(Instant.ofEpochSecond(it)).toString() - }, - artists = listOfNotNull( - Artist( - id = artistObject["ART_ID"]?.jsonPrimitive?.content.orEmpty(), - name = artistObject["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(artistMd5, "artist") - ) - ), - isExplicit = data["EXPLICIT_LYRICS"]?.jsonPrimitive?.content?.equals("1") ?: false, - extras = mapOf( - "TRACK_TOKEN" to data["TRACK_TOKEN"]?.jsonPrimitive?.content.orEmpty(), - "FILESIZE_MP3_MISC" to (data["FILESIZE_MP3_MISC"]?.jsonPrimitive?.content ?: "0"), - "MD5" to md5, - "TYPE" to "cover" - ) - ) -} - -fun JsonObject.toPlaylist(loaded: Boolean = false): Playlist { - val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this - val type = data["PICTURE_TYPE"]?.jsonPrimitive?.content.orEmpty() - val md5 = data["PLAYLIST_PICTURE"]?.jsonPrimitive?.content.orEmpty() - return Playlist( - id = data["PLAYLIST_ID"]?.jsonPrimitive?.content.orEmpty(), - title = data["TITLE"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, type, loaded), - description = data["DESCRIPTION"]?.jsonPrimitive?.content.orEmpty(), - subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty(), - isEditable = data["PARENT_USER_ID"]?.jsonPrimitive?.content == DeezerCredentialsHolder.credentials?.userId, - tracks = data["NB_SONG"]?.jsonPrimitive?.int ?: 0 - ) -} - -fun JsonObject.toRadio(loaded: Boolean = false): Radio { - val data = this["data"]?.jsonObject ?: this - val imageObject = this["pictures"]?.jsonArray?.firstOrNull()?.jsonObject.orEmpty() - val md5 = imageObject["md5"]?.jsonPrimitive?.content.orEmpty() - val type = imageObject["type"]?.jsonPrimitive?.content.orEmpty() - return Radio( - id = data["id"]?.jsonPrimitive?.content.orEmpty(), - title = data["title"]?.jsonPrimitive?.content.orEmpty(), - cover = getCover(md5, type, loaded), - extras = mapOf( - "radio" to "flow" - ) - ) -} - -private val quality: Int? get() = DeezerUtils.settings?.getInt("image_quality") - -fun getCover(md5: String?, type: String?, loaded: Boolean = false): ImageHolder { - if(loaded) { - val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/${quality ?: 240}x${quality ?: 240}-000000-80-0-0.jpg" - return url.toImageHolder() - } else { - val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/264x264-000000-80-0-0.jpg" - return url.toImageHolder() - } -} \ No newline at end of file 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 32cf8c8..b653b41 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt @@ -39,33 +39,30 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -class DeezerApi { +class DeezerApi(private val session: DeezerSession) { + init { - // Ensure credentials are initialized when the API is first used - if (DeezerCredentialsHolder.credentials == null) { - // Example initialization with placeholder values - DeezerCredentialsHolder.initialize( - DeezerCredentials( - arl = "", - sid = "", - token = "", - userId = "", - licenseToken = "", - email = "", - pass = "" - ) + if (session.credentials == null) { + session.credentials = DeezerCredentials( + arl = "", + sid = "", + token = "", + userId = "", + licenseToken = "", + email = "", + pass = "" ) } } private val language: String - get() = DeezerUtils.settings?.getString("lang") ?: Locale.getDefault().toLanguageTag() + get() = session.settings?.getString("lang") ?: Locale.getDefault().toLanguageTag() private val country: String - get() = DeezerUtils.settings?.getString("country") ?: Locale.getDefault().country + get() = session.settings?.getString("country") ?: Locale.getDefault().country private val credentials: DeezerCredentials - get() = DeezerCredentialsHolder.credentials ?: throw IllegalStateException("DeezerCredentials not initialized") + get() = session.credentials ?: throw IllegalStateException("DeezerCredentials not initialized") private val arl: String get() = credentials.arl @@ -101,7 +98,7 @@ class DeezerApi { originalResponse } } - if (useProxy && DeezerUtils.settings?.getBoolean("proxy") == true) { + if (useProxy && session.settings?.getBoolean("proxy") == true) { sslSocketFactory(createTrustAllSslSocketFactory(), createTrustAllTrustManager()) hostnameVerifier { _, _ -> true } proxy( @@ -197,17 +194,17 @@ class DeezerApi { if (method == "deezer.getUserData") { response.headers.forEach { if (it.second.startsWith("sid=")) { - DeezerCredentialsHolder.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) + session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) } } } if (responseBody.contains("\"VALID_TOKEN_REQUIRED\":\"Invalid CSRF token\"")) { if (email.isEmpty() && pass.isEmpty()) { - DeezerUtils.setArlExpired(true) + session.isArlExpired(true) throw Exception("Please re-login (Best use User + Pass method)") } else { - DeezerUtils.setArlExpired(false) + session.isArlExpired(false) val userList = DeezerExtension().onLogin(email, pass) DeezerExtension().onSetLoginUser(userList.first()) return@withContext callApi(method, params, gatewayInput) @@ -264,12 +261,12 @@ class DeezerApi { // Get access token val responseJson = getToken(params) val apiResponse = json.decodeFromString(responseJson) - DeezerCredentialsHolder.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) + session.updateCredentials(token = apiResponse.jsonObject["access_token"]!!.jsonPrimitive.content) // Get ARL val arlResponse = callApi("user.getArl") val arlObject = json.decodeFromString(arlResponse) - DeezerCredentialsHolder.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) + session.updateCredentials(arl = arlObject["results"]!!.jsonPrimitive.content) } private fun md5(input: String): String { @@ -311,7 +308,7 @@ class DeezerApi { val response = client.newCall(request).execute() response.headers.forEach { if (it.second.startsWith("sid=")) { - DeezerCredentialsHolder.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) + session.updateCredentials(sid = it.second.substringAfter("sid=").substringBefore(";")) } } } diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerBase.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerBase.kt index 40b5882..316338e 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerBase.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerBase.kt @@ -3,16 +3,6 @@ package dev.brahmkshatriya.echo.extension import dev.brahmkshatriya.echo.common.settings.Settings import java.util.Locale -data class DeezerCredentials( - val arl: String, - val sid: String, - val token: String, - val userId: String, - val licenseToken: String, - val email: String, - val pass: String -) - object DeezerCountries { fun getDefaultCountryIndex(settings: Settings?): Int { val storedCountryCode = settings?.getString("countryCode") @@ -90,48 +80,4 @@ object DeezerCountries { "es-MX", "cs-CZ", "sk-SK", "sv-SE", "en-US", "ja-JP", "bg-BG", "da-DK", "fi-FI", "sl-SI", "uk-UA" ) -} - -object DeezerUtils { - var settings: Settings? = null - - private var _arlExpired: Boolean = false - val arlExpired: Boolean - get() = _arlExpired - - fun setArlExpired(expired: Boolean) { - _arlExpired = expired - } -} - -object DeezerCredentialsHolder { - @Volatile - private var _credentials: DeezerCredentials? = null - val credentials: DeezerCredentials? - get() = _credentials - - fun initialize(credentials: DeezerCredentials) { - if (_credentials == null) { - _credentials = credentials - } else { - throw IllegalStateException("Credentials are already initialized") - } - } - - private val lock = Any() - - fun updateCredentials(arl: String? = null, sid: String? = null, token: String? = null, userId: String? = null, licenseToken: String? = null, email: String? = null, pass: String? = null) { - synchronized(lock) { - _credentials = _credentials?.copy( - arl = arl ?: _credentials!!.arl, - sid = sid ?: _credentials!!.sid, - token = token ?: _credentials!!.token, - userId = userId ?: _credentials!!.userId, - licenseToken = licenseToken ?: _credentials!!.licenseToken, - email = email ?: _credentials!!.email, - pass = pass ?: _credentials!!.pass - ) ?: throw IllegalStateException("Credentials are not initialized") - } - } -} - +} \ 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 6746f27..62271a4 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt @@ -39,7 +39,6 @@ import dev.brahmkshatriya.echo.common.settings.SettingSwitch import dev.brahmkshatriya.echo.common.settings.Settings import dev.brahmkshatriya.echo.extension.DeezerCountries.getDefaultCountryIndex import dev.brahmkshatriya.echo.extension.DeezerCountries.getDefaultLanguageIndex -import dev.brahmkshatriya.echo.extension.DeezerUtils.settings import dev.brahmkshatriya.echo.extension.clients.DeezerAlbumClient import dev.brahmkshatriya.echo.extension.clients.DeezerArtistClient import dev.brahmkshatriya.echo.extension.clients.DeezerHomeFeedClient @@ -63,7 +62,9 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC LoginClient.WebView.Cookie, LoginClient.UsernamePassword, LoginClient.CustomTextInput, LibraryClient, PlaylistEditClient, SaveToLibraryClient { - private val api = DeezerApi() + private val session = DeezerSession.getInstance() + private val api = DeezerApi(session) + private val parser = DeezerParser(session) override val settingItems: List get() = listOf( @@ -118,7 +119,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC "Choose your preferred language for loaded stuff", DeezerCountries.languageEntryTitles, DeezerCountries.languageEntryValues, - getDefaultLanguageIndex(settings) + getDefaultLanguageIndex(session.settings) ), SettingList( "Country", @@ -126,25 +127,21 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC "Choose your preferred country for browse recommendations", DeezerCountries.countryEntryTitles, DeezerCountries.countryEntryValues, - getDefaultCountryIndex(settings) + getDefaultCountryIndex(session.settings) ) ) ), ) - init { - initializeCredentials() - } - override fun setSettings(settings: Settings) { - DeezerUtils.settings = settings + session.settings = settings } override suspend fun onExtensionSelected() {} //<============= HomeTab =============> - private val deezerHomeFeedClient = DeezerHomeFeedClient(api) + private val deezerHomeFeedClient = DeezerHomeFeedClient(api, parser) override suspend fun getHomeTabs(): List = listOf() @@ -152,7 +149,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Library =============> - private val deezerLibraryClient = DeezerLibraryClient(api) + private val deezerLibraryClient = DeezerLibraryClient(api, parser) override suspend fun getLibraryTabs(): List = deezerLibraryClient.getLibraryTabs() @@ -222,7 +219,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC val playlistObject = tabObject["playlists"]!!.jsonObject val dataArray = playlistObject["data"]!!.jsonArray dataArray.map { - val playlist = it.jsonObject.toPlaylist() + val playlist = parser.run { it.jsonObject.toPlaylist() } if (playlist.isEditable) { playlistList.add(playlist) } @@ -303,7 +300,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Search =============> - private val deezerSearchClient = DeezerSearchClient(api, history) + private val deezerSearchClient = DeezerSearchClient(api, history, parser) override suspend fun quickSearch(query: String?): List = deezerSearchClient.quickSearch(query) @@ -322,7 +319,9 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC return coroutineScope { channelSections.map { section -> async(Dispatchers.IO) { - section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content) + parser.run { + section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content) + } } }.awaitAll().filterNotNull() } @@ -330,7 +329,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Play =============> - private val deezerTrackClient = DeezerTrackClient(api) + private val deezerTrackClient = DeezerTrackClient(api, parser) override suspend fun getStreamableMedia(streamable: Streamable): Streamable.Media = deezerTrackClient.getStreamableMedia(streamable) @@ -340,7 +339,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Radio =============> - private val deezerRadioClient = DeezerRadioClient(api) + private val deezerRadioClient = DeezerRadioClient(api, parser) override fun loadTracks(radio: Radio): PagedData = deezerRadioClient.loadTracks(radio) @@ -366,7 +365,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Album =============> - private val deezerAlbumClient = DeezerAlbumClient(api) + private val deezerAlbumClient = DeezerAlbumClient(api, parser) override fun getShelves(album: Album): PagedData.Single = getShelves(album.artists.first()) @@ -376,7 +375,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Playlist =============> - private val deezerPlaylistClient = DeezerPlaylistClient(api) + private val deezerPlaylistClient = DeezerPlaylistClient(api, parser) override fun getShelves(playlist: Playlist): PagedData.Single = deezerPlaylistClient.getShelves(playlist) @@ -386,7 +385,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Artist =============> - private val deezerArtistClient = DeezerArtistClient(api) + private val deezerArtistClient = DeezerArtistClient(api, parser) override fun getShelves(artist: Artist): PagedData.Single = deezerArtistClient.getShelves(artist) @@ -425,7 +424,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC val arl = extractCookieValue(data, "arl") val sid = extractCookieValue(data, "sid") if (arl != null && sid != null) { - DeezerCredentialsHolder.updateCredentials(arl = arl, sid = sid) + session.updateCredentials(arl = arl, sid = sid) return api.makeUser() } else { throw Exception("Failed to retrieve ARL and SID from cookies") @@ -448,7 +447,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC ) override suspend fun onLogin(data: Map): List { - DeezerCredentialsHolder.updateCredentials(arl = data["arl"] ?: "") + session.updateCredentials(arl = data["arl"] ?: "") api.getSid() val userList = api.makeUser() return userList @@ -456,8 +455,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC override suspend fun onLogin(username: String, password: String): List { // Set shared credentials - DeezerCredentialsHolder.updateCredentials(email = username) - DeezerCredentialsHolder.updateCredentials(pass = password) + session.updateCredentials(email = username, pass = password) api.getArlByEmail(username, password) val userList = api.makeUser(username, password) @@ -466,7 +464,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC override suspend fun onSetLoginUser(user: User?) { if (user != null) { - DeezerCredentialsHolder.updateCredentials( + session.updateCredentials( arl = user.extras["arl"] ?: "", sid = user.extras["sid"] ?: "", token = user.extras["token"] ?: "", @@ -493,39 +491,15 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeC //<============= Utils =============> - private fun initializeCredentials() { - if (DeezerCredentialsHolder.credentials == null) { - DeezerCredentialsHolder.initialize( - DeezerCredentials( - arl = "", - sid = "", - token = "", - userId = "", - licenseToken = "", - email = "", - pass = "" - ) - ) - } - } - fun handleArlExpiration() { - if (arl.isEmpty() || arlExpired) throw ClientException.LoginRequired() + if (session.credentials?.arl?.isEmpty() == true || session.arlExpired) throw ClientException.LoginRequired() } - private val arl: String get() = credentials.arl - private val arlExpired: Boolean get() = utils.arlExpired - private val credentials: DeezerCredentials - get() = DeezerCredentialsHolder.credentials ?: throw IllegalStateException( - LOGIN_REQUIRED_MESSAGE - ) - private val utils: DeezerUtils get() = DeezerUtils - private val quality: String get() = settings?.getString("audio_quality") ?: DEFAULT_QUALITY - private val log: Boolean get() = settings?.getBoolean("log") ?: false - private val history: Boolean get() = settings?.getBoolean("history") ?: true + private val quality: String get() = session.settings?.getString("audio_quality") ?: DEFAULT_QUALITY + private val log: Boolean get() = session.settings?.getBoolean("log") ?: false + private val history: Boolean get() = session.settings?.getBoolean("history") ?: true companion object { private const val DEFAULT_QUALITY = "320" - private const val LOGIN_REQUIRED_MESSAGE = "DeezerCredentials not initialized" } } \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerParser.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerParser.kt new file mode 100644 index 0000000..0d71a8e --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerParser.kt @@ -0,0 +1,256 @@ +package dev.brahmkshatriya.echo.extension + +import dev.brahmkshatriya.echo.common.helpers.PagedData +import dev.brahmkshatriya.echo.common.models.Album +import dev.brahmkshatriya.echo.common.models.Artist +import dev.brahmkshatriya.echo.common.models.EchoMediaItem +import dev.brahmkshatriya.echo.common.models.ImageHolder +import dev.brahmkshatriya.echo.common.models.ImageHolder.Companion.toImageHolder +import dev.brahmkshatriya.echo.common.models.Playlist +import dev.brahmkshatriya.echo.common.models.Radio +import dev.brahmkshatriya.echo.common.models.Shelf +import dev.brahmkshatriya.echo.common.models.Streamable +import dev.brahmkshatriya.echo.common.models.Track +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.time.Instant +import java.util.Date + +class DeezerParser(private val session: DeezerSession) { + + fun JsonElement.toShelfItemsList(name: String = "Unknown"): Shelf? { + val itemsArray = jsonObject["items"]?.jsonArray ?: return null + val list = itemsArray.mapNotNull { it.jsonObject.toEchoMediaItem() } + return if (list.isNotEmpty()) { + Shelf.Lists.Items( + title = name, + list = list + ) + } else { + null + } + } + + fun JsonObject.toShelfItemsList(name: String = "Unknown"): Shelf? { + val item = toEchoMediaItem() ?: return null + return Shelf.Lists.Items( + title = name, + list = listOf(item) + ) + } + + fun JsonArray.toShelfItemsList(name: String = "Unknown"): Shelf? { + val list = mapNotNull { it.jsonObject.toEchoMediaItem() } + return if (list.isNotEmpty()) { + Shelf.Lists.Items( + title = name, + list = list + ) + } else { + null + } + } + + fun JsonElement.toShelfCategoryList( + name: String = "Unknown", + block: suspend (String) -> List + ): Shelf.Lists.Categories { + val itemsArray = jsonObject["items"]?.jsonArray ?: return Shelf.Lists.Categories(name, emptyList()) + return Shelf.Lists.Categories( + title = name, + list = itemsArray.take(5).mapNotNull { it.jsonObject.toShelfCategory(block) }, + type = Shelf.Lists.Type.Linear, + more = PagedData.Single { + itemsArray.mapNotNull { it.jsonObject.toShelfCategory(block) } + } + ) + } + + fun JsonObject.toShelfCategory(block: suspend (String) -> List): Shelf.Category? { + val data = this["data"]?.jsonObject ?: this + val type = data["__TYPE__"]?.jsonPrimitive?.content ?: return null + return when { + "channel" in type -> toChannel(block) + else -> null + } + } + + fun JsonObject.toChannel(block: suspend (String) -> List): Shelf.Category { + val data = this["data"]?.jsonObject ?: this + val title = data["title"]?.jsonPrimitive?.content.orEmpty() + val target = this["target"]?.jsonPrimitive?.content.orEmpty() + return Shelf.Category( + title = title, + items = PagedData.Single { + block(target) + }, + ) + } + + fun JsonObject.toEchoMediaItem(): EchoMediaItem? { + val data = this["data"]?.jsonObject ?: this + val type = data["__TYPE__"]?.jsonPrimitive?.content ?: return null + return when { + "playlist" in type -> EchoMediaItem.Lists.PlaylistItem(toPlaylist()) + "album" in type -> EchoMediaItem.Lists.AlbumItem(toAlbum()) + "song" in type -> EchoMediaItem.TrackItem(toTrack()) + "artist" in type -> EchoMediaItem.Profile.ArtistItem(toArtist()) + "show" in type -> EchoMediaItem.Lists.AlbumItem(toShow()) + "episode" in type -> EchoMediaItem.TrackItem(toEpisode()) + "flow" in type -> EchoMediaItem.Lists.RadioItem(toRadio()) + else -> null + } + } + + fun JsonObject.toShow(loaded: Boolean = false): Album { + val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this + val md5 = data["SHOW_ART_MD5"]?.jsonPrimitive?.content.orEmpty() + return Album( + id = data["SHOW_ID"]?.jsonPrimitive?.content.orEmpty(), + title = data["SHOW_NAME"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, "talk", loaded), + tracks = this["EPISODES"]?.jsonObject?.get("total")?.jsonPrimitive?.int, + artists = listOf(Artist(id = "", name = "")), + description = data["SHOW_DESCRIPTION"]?.jsonPrimitive?.content.orEmpty(), + extras = mapOf("__TYPE__" to "show") + ) + } + + fun JsonObject.toEpisode(): Track { + val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this + val md5 = data["SHOW_ART_MD5"]?.jsonPrimitive?.content.orEmpty() + val title = data["EPISODE_TITLE"]?.jsonPrimitive?.content.orEmpty() + return Track( + id = data["EPISODE_ID"]?.jsonPrimitive?.content.orEmpty(), + title = title, + cover = getCover(md5, "talk", false), + duration = data["DURATION"]?.jsonPrimitive?.content?.toLongOrNull()?.times(1000), + streamables = listOf( + Streamable.audio( + id = data["EPISODE_DIRECT_STREAM_URL"]?.jsonPrimitive?.content.orEmpty(), + title = title, + quality = 1 + ) + ), + extras = mapOf( + "TRACK_TOKEN" to data["TRACK_TOKEN"]?.jsonPrimitive?.content.orEmpty(), + "FILESIZE_MP3_MISC" to (data["FILESIZE_MP3_MISC"]?.jsonPrimitive?.content ?: "0"), + "MD5" to md5, + "TYPE" to "talk", + "__TYPE__" to "show" + ) + ) + } + + fun JsonObject.toAlbum(loaded: Boolean = false): Album { + val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this + val md5 = data["ALB_PICTURE"]?.jsonPrimitive?.content.orEmpty() + val artistObject = data["ARTISTS"]?.jsonArray?.firstOrNull()?.jsonObject + val artistMd5 = artistObject?.get("ART_PICTURE")?.jsonPrimitive?.content.orEmpty() + return Album( + id = data["ALB_ID"]?.jsonPrimitive?.content.orEmpty(), + title = data["ALB_TITLE"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, "cover", loaded), + tracks = this["SONGS"]?.jsonObject?.get("total")?.jsonPrimitive?.int, + artists = listOfNotNull( + artistObject?.let { + Artist( + id = it["ART_ID"]?.jsonPrimitive?.content.orEmpty(), + name = it["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(artistMd5, "artist") + ) + } + ), + description = this["description"]?.jsonPrimitive?.content.orEmpty(), + subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty() + ) + } + + fun JsonObject.toArtist(isFollowing: Boolean = false, loaded: Boolean = false): Artist { + val data = this["data"]?.jsonObject ?: this + val md5 = data["ART_PICTURE"]?.jsonPrimitive?.content.orEmpty() + return Artist( + id = data["ART_ID"]?.jsonPrimitive?.content.orEmpty(), + name = data["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, "artist", loaded), + description = this["description"]?.jsonPrimitive?.content.orEmpty(), + subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty(), + isFollowing = isFollowing + ) + } + + @Suppress("NewApi") + fun JsonObject.toTrack(loaded: Boolean = false): Track { + val data = this["data"]?.jsonObject ?: this + val md5 = data["ALB_PICTURE"]?.jsonPrimitive?.content.orEmpty() + val artistObject = data["ARTISTS"]?.jsonArray?.firstOrNull()?.jsonObject ?: data + val artistMd5 = artistObject["ART_PICTURE"]?.jsonPrimitive?.content.orEmpty() + return Track( + id = data["SNG_ID"]?.jsonPrimitive?.content.orEmpty(), + title = data["SNG_TITLE"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, "cover", loaded), + duration = data["DURATION"]?.jsonPrimitive?.content?.toLongOrNull()?.times(1000), + releaseDate = data["DATE_ADD"]?.jsonPrimitive?.content?.toLongOrNull()?.let { + Date.from(Instant.ofEpochSecond(it)).toString() + }, + artists = listOfNotNull( + Artist( + id = artistObject["ART_ID"]?.jsonPrimitive?.content.orEmpty(), + name = artistObject["ART_NAME"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(artistMd5, "artist") + ) + ), + isExplicit = data["EXPLICIT_LYRICS"]?.jsonPrimitive?.content?.equals("1") ?: false, + extras = mapOf( + "TRACK_TOKEN" to data["TRACK_TOKEN"]?.jsonPrimitive?.content.orEmpty(), + "FILESIZE_MP3_MISC" to (data["FILESIZE_MP3_MISC"]?.jsonPrimitive?.content ?: "0"), + "MD5" to md5, + "TYPE" to "cover" + ) + ) + } + + fun JsonObject.toPlaylist(loaded: Boolean = false): Playlist { + val data = this["data"]?.jsonObject ?: this["DATA"]?.jsonObject ?: this + val type = data["PICTURE_TYPE"]?.jsonPrimitive?.content.orEmpty() + val md5 = data["PLAYLIST_PICTURE"]?.jsonPrimitive?.content.orEmpty() + return Playlist( + id = data["PLAYLIST_ID"]?.jsonPrimitive?.content.orEmpty(), + title = data["TITLE"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, type, loaded), + description = data["DESCRIPTION"]?.jsonPrimitive?.content.orEmpty(), + subtitle = this["subtitle"]?.jsonPrimitive?.content.orEmpty(), + isEditable = data["PARENT_USER_ID"]?.jsonPrimitive?.content == session.credentials?.userId, + tracks = data["NB_SONG"]?.jsonPrimitive?.int ?: 0 + ) + } + + private fun JsonObject.toRadio(loaded: Boolean = false): Radio { + val data = this["data"]?.jsonObject ?: this + val imageObject = this["pictures"]?.jsonArray?.firstOrNull()?.jsonObject.orEmpty() + val md5 = imageObject["md5"]?.jsonPrimitive?.content.orEmpty() + val type = imageObject["type"]?.jsonPrimitive?.content.orEmpty() + return Radio( + id = data["id"]?.jsonPrimitive?.content.orEmpty(), + title = data["title"]?.jsonPrimitive?.content.orEmpty(), + cover = getCover(md5, type, loaded), + extras = mapOf( + "radio" to "flow" + ) + ) + } + + private val quality: Int? + get() = session.settings?.getInt("image_quality") + + private fun getCover(md5: String?, type: String?, loaded: Boolean = false): ImageHolder { + val size = if (loaded) "${quality ?: 240}" else "264" + val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/${size}x${size}-000000-80-0-0.jpg" + return url.toImageHolder() + } +} \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerSession.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerSession.kt new file mode 100644 index 0000000..bf5ce85 --- /dev/null +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerSession.kt @@ -0,0 +1,59 @@ +package dev.brahmkshatriya.echo.extension + +import dev.brahmkshatriya.echo.common.settings.Settings + +class DeezerSession( + var credentials: DeezerCredentials? = null, + var settings: Settings? = null, + var arlExpired: Boolean = false +) { + private val lock = Any() + + fun updateCredentials( + arl: String? = null, + sid: String? = null, + token: String? = null, + userId: String? = null, + licenseToken: String? = null, + email: String? = null, + pass: String? = null + ) { + synchronized(lock) { + val current = credentials ?: DeezerCredentials("", "", "", "", "", "", "") + credentials = current.copy( + arl = arl ?: current.arl, + sid = sid ?: current.sid, + token = token ?: current.token, + userId = userId ?: current.userId, + licenseToken = licenseToken ?: current.licenseToken, + email = email ?: current.email, + pass = pass ?: current.pass + ) + } + } + + fun isArlExpired(expired: Boolean) { + arlExpired = expired + } + + companion object { + @Volatile + private var instance: DeezerSession? = null + + fun getInstance(): DeezerSession { + return instance ?: synchronized(this) { + instance ?: DeezerSession().also { instance = it } + } + } + } +} + +data class DeezerCredentials( + val arl: String, + val sid: String, + val token: String, + val userId: String, + val licenseToken: String, + val email: String, + val pass: String +) \ No newline at end of file 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 506e636..96e8e21 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt @@ -76,21 +76,22 @@ object Utils { } suspend fun getByteChannel( - scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + scope: CoroutineScope, streamable: Streamable, client: OkHttpClient, contentLength: Long -): ByteChannel = withContext(Dispatchers.IO) { +): ByteChannel { val url = streamable.id val key = streamable.extra["key"] ?: "" val byteChannel = ByteChannel(true) + var lastActivityTime = System.currentTimeMillis() scope.launch { val clientWithTimeouts = client.newBuilder() - .readTimeout(60, TimeUnit.SECONDS) - .connectTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .connectTimeout(0, TimeUnit.SECONDS) + .writeTimeout(0, TimeUnit.SECONDS) .connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES)) .protocols(listOf(Protocol.HTTP_1_1)) .retryOnConnectionFailure(true) @@ -113,7 +114,12 @@ suspend fun getByteChannel( response.body.byteStream().use { byteStream -> var shouldReopen = false - while (totalBytesRead < contentLength) { + while (totalBytesRead < contentLength && !shouldReopen) { + if (System.currentTimeMillis() - lastActivityTime > 300_000L) { + shouldReopen = true + break + } + val buffer = ByteArray(2048) var bytesRead: Int var totalRead = 0 @@ -126,6 +132,8 @@ suspend fun getByteChannel( break } totalRead += bytesRead + + lastActivityTime = System.currentTimeMillis() } } catch (e: Exception) { e.printStackTrace() @@ -139,6 +147,11 @@ suspend fun getByteChannel( } try { + if (System.currentTimeMillis() - lastActivityTime > 300_000L) { + shouldReopen = true + break + } + if (totalRead != 2048) { byteChannel.writeFully(buffer, 0, totalRead) } else { @@ -149,6 +162,8 @@ suspend fun getByteChannel( byteChannel.writeFully(buffer, 0, totalRead) } } + + lastActivityTime = System.currentTimeMillis() } catch (e: IOException) { e.printStackTrace() println("Exception occurred while writing to channel: ${e.message}") @@ -166,11 +181,11 @@ suspend fun getByteChannel( } } - byteChannel + return byteChannel } suspend fun getByteStreamAudio( - scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + scope: CoroutineScope, streamable: Streamable, client: OkHttpClient ): Streamable.Media { diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerAlbumClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerAlbumClient.kt index 0331753..0289a2f 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerAlbumClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerAlbumClient.kt @@ -4,24 +4,21 @@ import dev.brahmkshatriya.echo.common.helpers.PagedData import dev.brahmkshatriya.echo.common.models.Album import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.extension.DeezerApi -import dev.brahmkshatriya.echo.extension.toAlbum -import dev.brahmkshatriya.echo.extension.toEpisode -import dev.brahmkshatriya.echo.extension.toShow -import dev.brahmkshatriya.echo.extension.toTrack +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -class DeezerAlbumClient(private val api: DeezerApi) { +class DeezerAlbumClient(private val api: DeezerApi, private val parser: DeezerParser) { suspend fun loadAlbum(album: Album): Album { if (album.extras["__TYPE__"] == "show") { val jsonObject = api.show(album) val resultsObject = jsonObject["results"]!!.jsonObject - return resultsObject.toShow(true) + return parser.run { resultsObject.toShow(true) } } else { val jsonObject = api.album(album) val resultsObject = jsonObject["results"]!!.jsonObject - return resultsObject.toAlbum(true) + return parser.run { resultsObject.toAlbum(true) } } } @@ -32,7 +29,9 @@ class DeezerAlbumClient(private val api: DeezerApi) { val episodesObject = resultsObject["EPISODES"]!!.jsonObject val dataArray = episodesObject["data"]!!.jsonArray val data = dataArray.map { episode -> - episode.jsonObject.toEpisode() + parser.run { + episode.jsonObject.toEpisode() + } }.reversed() data } else { @@ -41,8 +40,8 @@ class DeezerAlbumClient(private val api: DeezerApi) { val songsObject = resultsObject["SONGS"]!!.jsonObject val dataArray = songsObject["data"]!!.jsonArray val data = dataArray.mapIndexed { index, song -> - val currentTrack = song.jsonObject.toTrack() - val nextTrack = dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() + val currentTrack = parser.run { song.jsonObject.toTrack() } + val nextTrack = parser.run { dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() } Track( id = currentTrack.id, title = currentTrack.title, diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerArtistClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerArtistClient.kt index 3e85479..d246a92 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerArtistClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerArtistClient.kt @@ -5,21 +5,20 @@ import dev.brahmkshatriya.echo.common.models.Artist import dev.brahmkshatriya.echo.common.models.EchoMediaItem import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.extension.DeezerApi -import dev.brahmkshatriya.echo.extension.toArtist -import dev.brahmkshatriya.echo.extension.toShelfItemsList +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -class DeezerArtistClient(private val api: DeezerApi) { +class DeezerArtistClient(private val api: DeezerApi, private val parser: DeezerParser) { fun getShelves(artist: Artist): PagedData.Single = PagedData.Single { try { val jsonObject = api.artist(artist.id) val resultsObject = jsonObject["results"]!!.jsonObject - val keyToBlock: Map Shelf?> = mapOf( + val keyToBlock: Map Shelf?> = parser.run { mapOf( "TOP" to { jObject -> val shelf = jObject["data"]?.jsonArray?.toShelfItemsList("Top") as Shelf.Lists.Items @@ -53,11 +52,23 @@ class DeezerArtistClient(private val api: DeezerApi) { "ALBUMS" to { jObject -> jObject["data"]?.jsonArray?.toShelfItemsList("Albums") } + ) } + + val orderedKeys = listOf( + "TOP", + "HIGHLIGHT", + "SELECTED_PLAYLIST", + "ALBUMS", + "RELATED_PLAYLIST", + "RELATED_ARTISTS" ) - resultsObject.mapNotNull { (key, value) -> + orderedKeys.mapNotNull { key -> + val value = resultsObject[key] val block = keyToBlock[key] - block?.invoke(value.jsonObject) + if (value != null && block != null) { + block.invoke(value.jsonObject) + } else null } } catch (e: Exception) { emptyList() @@ -69,7 +80,7 @@ class DeezerArtistClient(private val api: DeezerApi) { val resultsObject = jsonObject["results"]?.jsonObject?.get("DATA")?.jsonObject ?: return artist val isFollowing = isFollowingArtist(artist.id) - return resultsObject.toArtist(isFollowing, true) + return parser.run { resultsObject.toArtist(isFollowing, true) } } private suspend fun isFollowingArtist(id: String): Boolean { 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 368a8d6..20f4f05 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 @@ -4,14 +4,13 @@ import dev.brahmkshatriya.echo.common.helpers.PagedData import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.extension.DeezerApi import dev.brahmkshatriya.echo.extension.DeezerExtension -import dev.brahmkshatriya.echo.extension.toShelfCategoryList -import dev.brahmkshatriya.echo.extension.toShelfItemsList +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -class DeezerHomeFeedClient(private val api: DeezerApi) { +class DeezerHomeFeedClient(private val api: DeezerApi, private val parser: DeezerParser) { fun getHomeFeed(): PagedData = PagedData.Single { DeezerExtension().handleArlExpiration() @@ -26,12 +25,16 @@ class DeezerHomeFeedClient(private val api: DeezerApi) { "7a65f4ed-71e1-4b6e-97ba-4de792e4af62", "25f9200f-1ce0-45eb-abdc-02aecf7604b2", "c320c7ad-95f5-4021-8de1-cef16b053b6d", "b2e8249f-8541-479e-ab90-cf4cf5896cbc", "927121fd-ef7b-428e-8214-ae859435e51c" -> { - section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content) + parser.run { + section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content) + } } "868606eb-4afc-4e1a-b4e4-75b30da34ac8" -> { - section.toShelfCategoryList(section.jsonObject["title"]!!.jsonPrimitive.content) { target -> - DeezerExtension().channelFeed(target) + parser.run { + section.toShelfCategoryList(section.jsonObject["title"]!!.jsonPrimitive.content) { target -> + DeezerExtension().channelFeed(target) + } } } diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLibraryClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLibraryClient.kt index 155de9f..42d3f59 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLibraryClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLibraryClient.kt @@ -5,8 +5,7 @@ import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.common.models.Tab import dev.brahmkshatriya.echo.extension.DeezerApi import dev.brahmkshatriya.echo.extension.DeezerExtension -import dev.brahmkshatriya.echo.extension.toEchoMediaItem -import dev.brahmkshatriya.echo.extension.toShelfItemsList +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -15,7 +14,7 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -class DeezerLibraryClient(private val api: DeezerApi) { +class DeezerLibraryClient(private val api: DeezerApi, private val parser: DeezerParser) { @Volatile private var allTabs: Pair>? = null @@ -53,8 +52,9 @@ class DeezerLibraryClient(private val api: DeezerApi) { "tracks" -> resultObject["data"]?.jsonArray else -> return@async null } - - dataArray?.toShelfItemsList(tab.name) + parser.run { + dataArray?.toShelfItemsList(tab.name) + } } }.awaitAll().filterNotNull() } @@ -88,7 +88,9 @@ class DeezerLibraryClient(private val api: DeezerApi) { } return dataArray?.mapNotNull { item -> - item.jsonObject.toEchoMediaItem()?.toShelf() + parser.run { + item.jsonObject.toEchoMediaItem()?.toShelf() + } } ?: emptyList() } } \ No newline at end of file diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerPlaylistClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerPlaylistClient.kt index e0f23f7..bf18024 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerPlaylistClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerPlaylistClient.kt @@ -5,14 +5,12 @@ import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.extension.DeezerApi -import dev.brahmkshatriya.echo.extension.toPlaylist -import dev.brahmkshatriya.echo.extension.toShelfItemsList -import dev.brahmkshatriya.echo.extension.toTrack +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -class DeezerPlaylistClient(private val api: DeezerApi) { +class DeezerPlaylistClient(private val api: DeezerApi, private val parser: DeezerParser) { fun getShelves(playlist: Playlist): PagedData.Single = PagedData.Single { val jsonObject = api.playlist(playlist) @@ -20,7 +18,9 @@ class DeezerPlaylistClient(private val api: DeezerApi) { val songsObject = resultsObject["SONGS"]!!.jsonObject val dataArray = songsObject["data"]?.jsonArray ?: JsonArray(emptyList()) val data = dataArray.mapNotNull { song -> - song.jsonObject.toShelfItemsList(name = "") + parser.run { + song.jsonObject.toShelfItemsList(name = "") + } } //data emptyList() @@ -29,15 +29,15 @@ class DeezerPlaylistClient(private val api: DeezerApi) { suspend fun loadPlaylist(playlist: Playlist): Playlist { val jsonObject = api.playlist(playlist) val resultsObject = jsonObject["results"]!!.jsonObject - return resultsObject.toPlaylist(true) + return parser.run { resultsObject.toPlaylist(true) } } fun loadTracks(playlist: Playlist): PagedData = PagedData.Single { val jsonObject = api.playlist(playlist) val dataArray = jsonObject["results"]!!.jsonObject["SONGS"]!!.jsonObject["data"]!!.jsonArray dataArray.mapIndexed { index, song -> - val currentTrack = song.jsonObject.toTrack() - val nextTrack = dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() + val currentTrack = parser.run { song.jsonObject.toTrack() } + val nextTrack = parser.run { dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() } Track( id = currentTrack.id, title = currentTrack.title, diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerRadioClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerRadioClient.kt index b7597c1..728bf6d 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerRadioClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerRadioClient.kt @@ -8,11 +8,11 @@ import dev.brahmkshatriya.echo.common.models.Playlist import dev.brahmkshatriya.echo.common.models.Radio import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.extension.DeezerApi -import dev.brahmkshatriya.echo.extension.toTrack +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -class DeezerRadioClient(private val api: DeezerApi) { +class DeezerRadioClient(private val api: DeezerApi, private val parser: DeezerParser) { fun loadTracks(radio: Radio): PagedData = PagedData.Single { val dataArray = when (radio.extras["radio"]) { @@ -38,8 +38,8 @@ class DeezerRadioClient(private val api: DeezerApi) { } dataArray.mapIndexed { index, song -> - val track = song.jsonObject.toTrack() - val nextTrack = dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() + val track = parser.run { song.jsonObject.toTrack() } + val nextTrack = parser.run { dataArray.getOrNull(index + 1)?.jsonObject?.toTrack() } val nextTrackId = nextTrack?.id Track( @@ -162,7 +162,7 @@ class DeezerRadioClient(private val api: DeezerApi) { val jsonObject = api.album(album) val resultsObject = jsonObject["results"]!!.jsonObject val songsObject = resultsObject["SONGS"]!!.jsonObject - val lastTrack = songsObject["data"]!!.jsonArray.reversed()[0].jsonObject.toTrack() + val lastTrack = parser.run { songsObject["data"]!!.jsonArray.reversed()[0].jsonObject.toTrack() } return Radio( id = lastTrack.id, title = lastTrack.title, @@ -178,7 +178,7 @@ class DeezerRadioClient(private val api: DeezerApi) { val jsonObject = api.playlist(playlist) val resultsObject = jsonObject["results"]!!.jsonObject val songsObject = resultsObject["SONGS"]!!.jsonObject - val lastTrack = songsObject["data"]!!.jsonArray.reversed()[0].jsonObject.toTrack() + val lastTrack = parser.run { songsObject["data"]!!.jsonArray.reversed()[0].jsonObject.toTrack() } return Radio( id = lastTrack.id, title = lastTrack.title, 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 3511f46..c766750 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 @@ -6,9 +6,7 @@ import dev.brahmkshatriya.echo.common.models.Shelf import dev.brahmkshatriya.echo.common.models.Tab import dev.brahmkshatriya.echo.extension.DeezerApi import dev.brahmkshatriya.echo.extension.DeezerExtension -import dev.brahmkshatriya.echo.extension.toEchoMediaItem -import dev.brahmkshatriya.echo.extension.toShelfCategoryList -import dev.brahmkshatriya.echo.extension.toShelfItemsList +import dev.brahmkshatriya.echo.extension.DeezerParser import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -19,7 +17,7 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import java.util.Locale -class DeezerSearchClient(private val api: DeezerApi, private val history: Boolean) { +class DeezerSearchClient(private val api: DeezerApi, private val history: Boolean, private val parser: DeezerParser) { @Volatile private var oldSearch: Pair>? = null @@ -80,7 +78,9 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea val dataArray = tabObject?.get("data")?.jsonArray dataArray?.mapNotNull { item -> - item.jsonObject.toEchoMediaItem()?.toShelf() + parser.run { + item.jsonObject.toEchoMediaItem()?.toShelf() + } } ?: emptyList() } @@ -99,12 +99,16 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea "67aa1c1b-7873-488d-88a0-55b6596cf4d6", "486313b7-e3c7-453d-ba79-27dc6bea20ce", "1d8dfed4-582f-40e1-b29c-760b44c0301e", "ecb89e7c-1c07-4922-aa50-d29745576636", "64ac680b-7c84-49a3-9077-38e9b653332e" -> { - section.toShelfItemsList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) + parser.run { + section.toShelfItemsList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) + } } "8b2c6465-874d-4752-a978-1637ca0227b5" -> { - section.toShelfCategoryList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) { target -> - DeezerExtension().channelFeed(target) + parser.run { + section.toShelfCategoryList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) { target -> + DeezerExtension().channelFeed(target) + } } } @@ -138,7 +142,9 @@ class DeezerSearchClient(private val api: DeezerApi, private val history: Boolea val name = tab.id val tabObject = resultObject?.get(name)?.jsonObject val dataArray = tabObject?.get("data")?.jsonArray - dataArray?.toShelfItemsList(name.lowercase().capitalize(Locale.ROOT)) + parser.run { + dataArray?.toShelfItemsList(name.lowercase().capitalize(Locale.ROOT)) + } } return listOf(Tab("All", "All")) + tabs } diff --git a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerTrackClient.kt b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerTrackClient.kt index d960883..01e94c0 100644 --- a/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerTrackClient.kt +++ b/ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerTrackClient.kt @@ -5,10 +5,10 @@ import dev.brahmkshatriya.echo.common.models.Streamable.Audio.Companion.toAudio import dev.brahmkshatriya.echo.common.models.Streamable.Media.Companion.toMedia import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.extension.DeezerApi +import dev.brahmkshatriya.echo.extension.DeezerParser import dev.brahmkshatriya.echo.extension.Utils import dev.brahmkshatriya.echo.extension.generateTrackUrl import dev.brahmkshatriya.echo.extension.getByteStreamAudio -import dev.brahmkshatriya.echo.extension.toTrack import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -19,7 +19,7 @@ import kotlinx.serialization.json.jsonPrimitive import okhttp3.OkHttpClient import okhttp3.Request -class DeezerTrackClient(private val api: DeezerApi) { +class DeezerTrackClient(private val api: DeezerApi, private val parser: DeezerParser) { private val client = OkHttpClient() @@ -38,7 +38,7 @@ class DeezerTrackClient(private val api: DeezerApi) { return trackObject["results"]!!.jsonObject["data"]!!.jsonArray[0].jsonObject } - val newTrack = fetchTrackData(track).toTrack(loaded = true).copy(extras = track.extras) + val newTrack = parser.run { fetchTrackData(track).toTrack(loaded = true).copy(extras = track.extras) } if (newTrack.extras["__TYPE__"] == "show") { return newTrack diff --git a/gradle.properties b/gradle.properties index d1beeb2..1b7e90e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ android.useAndroidX=true kotlin.code.style=official android.nonTransitiveRClass=true -libVersion=9b57d45210 +libVersion=3dbbb1d84d extType=music extId=deezer