Skip to content

Commit

Permalink
Update to new echo, add categories & more
Browse files Browse the repository at this point in the history
I also worked on the decryptor again. Please tell me if you have any issue.

The radio items currently aren't working. Wouldn't touch them if you don't want any crash.

Increased amount of songs that are loaded in the liked track tab
  • Loading branch information
LuftVerbot committed Sep 21, 2024
1 parent e3ef9e0 commit 740474f
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 108 deletions.
2 changes: 1 addition & 1 deletion ext/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies {

implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
implementation("com.squareup.okhttp3:okhttp-coroutines:5.0.0-alpha.14")
implementation("io.ktor:ktor-utils:2.3.0")
implementation("io.ktor:ktor-utils:3.0.0-beta-2")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
Expand Down
27 changes: 15 additions & 12 deletions ext/src/main/java/dev/brahmkshatriya/echo/extension/Convertors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,36 @@ fun JsonArray.toShelfItemsList(name: String = "Unknown"): Shelf {
)
}

fun JsonElement.toShelfCategoryList(name: String = "Unknown"): Shelf.Lists.Categories {
fun JsonElement.toShelfCategoryList(name: String = "Unknown", block: suspend (String) -> List<Shelf>): Shelf.Lists.Categories {
val itemsArray = jsonObject["items"]?.jsonArray ?: return Shelf.Lists.Categories(name, emptyList())
return Shelf.Lists.Categories(
title = name,
list = itemsArray.mapNotNull { it.jsonObject.toShelfCategory() },
type = Shelf.Lists.Type.Grid
list = itemsArray.take(5).mapNotNull { it.jsonObject.toShelfCategory(block) },
type = Shelf.Lists.Type.Grid,
more = PagedData.Single {
itemsArray.mapNotNull { it.jsonObject.toShelfCategory(block) }
}
)
}

fun JsonObject.toShelfCategory(): Shelf.Category? {
fun JsonObject.toShelfCategory(block: suspend (String) -> List<Shelf>): Shelf.Category? {
val data = this["data"]?.jsonObject ?: this
val type = data["__TYPE__"]?.jsonPrimitive?.content ?: return null
return when {
"channel" in type -> toChannel()
"channel" in type -> toChannel(block)
else -> null
}
}

fun JsonObject.toChannel(): Shelf.Category {
fun JsonObject.toChannel(block: suspend (String) -> List<Shelf>): 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.empty(),
extras = mapOf(
"target" to target
)
items = PagedData.Single {
block(target)
},
)
}

Expand Down Expand Up @@ -187,6 +189,7 @@ fun JsonObject.toTrack(): Track {
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"),
Expand Down Expand Up @@ -239,11 +242,11 @@ fun JsonObject.toRadio(loaded: Boolean = false): Radio {
)
}

private val quality: Int get() = DeezerUtils.settings?.getInt("image_quality") ?: 240
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}x$quality-000000-80-0-0.jpg"
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ class DeezerApi {
params = buildJsonObject {
put("user_id", userId)
put("tab", "loved")
put("nb", 50)
put("nb", 10000)
put("start", 0)
}
)
Expand Down Expand Up @@ -736,9 +736,9 @@ class DeezerApi {

suspend fun channelPage(target: String): JsonObject {
val jsonData = callApi(
method = "",
method = "page.get",
gatewayInput = """
{"PAGE":"$target","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":"${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"]}
""".trimIndent()
)
return json.decodeFromString<JsonObject>(jsonData)
Expand Down
126 changes: 76 additions & 50 deletions ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import dev.brahmkshatriya.echo.common.clients.SaveToLibraryClient
import dev.brahmkshatriya.echo.common.clients.SearchClient
import dev.brahmkshatriya.echo.common.clients.ShareClient
import dev.brahmkshatriya.echo.common.clients.TrackClient
import dev.brahmkshatriya.echo.common.clients.TrackLikeClient
import dev.brahmkshatriya.echo.common.helpers.Page
import dev.brahmkshatriya.echo.common.helpers.PagedData
import dev.brahmkshatriya.echo.common.models.Album
Expand Down Expand Up @@ -45,6 +46,7 @@ import dev.brahmkshatriya.echo.extension.DeezerCountries.getDefaultLanguageIndex
import dev.brahmkshatriya.echo.extension.DeezerUtils.settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
Expand All @@ -61,7 +63,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.Locale

class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClient, SearchClient, AlbumClient, ArtistClient,
class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, TrackLikeClient, RadioClient, SearchClient, AlbumClient, ArtistClient,
ArtistFollowClient, PlaylistClient, LyricsClient, ShareClient, LoginClient.WebView.Cookie,
LoginClient.UsernamePassword, LoginClient.CustomTextInput, LibraryClient, PlaylistEditClient,
SaveToLibraryClient {
Expand Down Expand Up @@ -105,6 +107,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
"Image Quality",
"image_quality",
"Choose your preferred image quality",
240,
120,
1920,
120
Expand Down Expand Up @@ -149,37 +152,36 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien

override suspend fun getHomeTabs() = listOf<Tab>()

override fun getHomeFeed(tab: Tab?): PagedData<Shelf> = PagedData.Continuous {
@OptIn(ExperimentalCoroutinesApi::class)
override fun getHomeFeed(tab: Tab?): PagedData<Shelf> = PagedData.Single {
handleArlExpiration()
val homeSections = api.homePage()["results"]?.jsonObject?.get("sections")?.jsonArray ?: JsonArray(emptyList())

val dataList = coroutineScope {
homeSections .map { section ->
withContext(Dispatchers.IO.limitedParallelism(4)) {
homeSections.map { section ->
val id = section.jsonObject["module_id"]!!.jsonPrimitive.content
async {
val id = section.jsonObject["module_id"]!!.jsonPrimitive.content
if (isSupportedSection(id)) {
section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content)
} else null
when (id) {
"b21892d3-7e9c-4b06-aff6-2c3be3266f68", "348128f5-bed6-4ccb-9a37-8e5f5ed08a62",
"8d10a320-f130-4dcb-a610-38baf0c57896", "2a7e897f-9bcf-4563-8e11-b93a601766e1",
"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)
}

"868606eb-4afc-4e1a-b4e4-75b30da34ac8" -> {
section.toShelfCategoryList(section.jsonObject["title"]!!.jsonPrimitive.content) { target ->
channelFeed(target)
}
}

else -> null
}
}
}.awaitAll().filterNotNull()
}

Page(dataList, it)
}


private fun isSupportedSection(id: String) = listOf(
"b21892d3-7e9c-4b06-aff6-2c3be3266f68",
"348128f5-bed6-4ccb-9a37-8e5f5ed08a62",
"8d10a320-f130-4dcb-a610-38baf0c57896",
"2a7e897f-9bcf-4563-8e11-b93a601766e1",
"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"
).contains(id)

//<============= Library =============>

@Volatile
Expand Down Expand Up @@ -284,14 +286,12 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
api.updatePlaylist(playlist.id, title, description)
}

override suspend fun likeTrack(track: Track, liked: Boolean): Boolean {
override suspend fun likeTrack(track: Track, isLiked: Boolean) {
handleArlExpiration()
if(liked) {
if(isLiked) {
api.addFavoriteTrack(track.id)
return true
} else {
api.removeFavoriteTrack(track.id)
return false
}
}

Expand Down Expand Up @@ -374,7 +374,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
}

is EchoMediaItem.Lists.PlaylistItem -> {
api.addFavoriteAlbum(mediaItem.playlist.id)
api.addFavoritePlaylist(mediaItem.playlist.id)
}

else -> {}
Expand Down Expand Up @@ -451,36 +451,51 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
} ?: emptyList()
}

@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun browseFeed(): List<Shelf> {
val dataList = mutableListOf<Shelf>()
handleArlExpiration()
api.updateCountry()
val jsonObject = api.browsePage()
val resultObject = jsonObject["results"]!!.jsonObject
val sections = resultObject["sections"]!!.jsonArray
val jsonData = json.decodeFromString<JsonArray>(sections.toString())
jsonData.map { section ->
val id = section.jsonObject["module_id"]!!.jsonPrimitive.content
// Just for the time being until everything is implemented
when(id) {
"67aa1c1b-7873-488d-88a0-55b6596cf4d6", "486313b7-e3c7-453d-ba79-27dc6bea20ce",
"1d8dfed4-582f-40e1-b29c-760b44c0301e" -> {
val name = section.jsonObject["title"]!!.jsonPrimitive.content
val data = section.toShelfItemsList(name = name)
dataList.add(data)
}
"8b2c6465-874d-4752-a978-1637ca0227b5" -> {
val name = section.jsonObject["title"]!!.jsonPrimitive.content
val categories = section.toShelfCategoryList(name = name)
val data = categories.list.map {
val target = it.extras["target"]
return withContext(Dispatchers.IO.limitedParallelism(4)) {
jsonData.mapNotNull { section ->
val id = section.jsonObject["module_id"]!!.jsonPrimitive.content
async {
when (id) {
"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())
}

"8b2c6465-874d-4752-a978-1637ca0227b5" -> {
section.toShelfCategoryList(section.jsonObject["title"]?.jsonPrimitive?.content.orEmpty()) { target ->
channelFeed(target)
}
}

else -> null
}
dataList.add(data)
}
}.awaitAll().filterNotNull()
}
}

else -> null
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun channelFeed(target: String): List<Shelf> {
val jsonObject = api.channelPage(target)
val resultObject = jsonObject["results"]!!.jsonObject
val sections = resultObject["sections"]!!.jsonArray
val jsonData = json.decodeFromString<JsonArray>(sections.toString())
return withContext(Dispatchers.IO.limitedParallelism(4)) {
jsonData.map { section ->
async {
section.toShelfItemsList(section.jsonObject["title"]!!.jsonPrimitive.content)
}
}.awaitAll()
}
return dataList
}

override suspend fun searchTabs(query: String?): List<Tab> {
Expand Down Expand Up @@ -520,11 +535,21 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
return if (streamable.quality == 1) {
streamable.id.toAudio().toMedia()
} else {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
getByteStreamAudio(scope, streamable, client)
}
}

private suspend fun isTrackLiked(id: String): Boolean {
val dataArray = api.getTracks()["results"]?.jsonObject
?.get("data")?.jsonArray ?: return false

return dataArray.any { item ->
val artistId = item.jsonObject["SNG_ID"]?.jsonPrimitive?.content
artistId == id
}
}

override suspend fun loadTrack(track: Track) = coroutineScope {
val newTrack = track.toNewTrack()

Expand Down Expand Up @@ -605,6 +630,7 @@ class DeezerExtension : ExtensionClient, HomeFeedClient, TrackClient, RadioClien
title = track.title,
cover = newTrack.cover,
artists = track.artists,
isLiked = isTrackLiked(track.id),
streamables = listOf(
Streamable.audio(
id = url,
Expand Down
Loading

0 comments on commit 740474f

Please sign in to comment.