Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: brahmkshatriya/echo-extension-template
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: LuftVerbot/echo-deezer-extension
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
Loading
Showing with 4,692 additions and 340 deletions.
  1. +16 −2 .github/workflows/build.yml
  2. +67 −74 README.md
  3. +3 −1 app/.gitignore
  4. +26 −1 app/proguard-rules.pro
  5. +17 −0 app/src/main/AndroidManifest.xml
  6. +112 −0 app/src/main/java/dev/brahmkshatriya/echo/link/Opener.kt
  7. +851 −6 app/src/main/res/drawable/ic_launcher_foreground.xml
  8. +0 −3 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  9. BIN app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  10. +5 −0 app/src/main/res/values/styles.xml
  11. +1 −7 ext/build.gradle.kts
  12. +501 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerApi.kt
  13. +142 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerBase.kt
  14. +535 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerExtension.kt
  15. +270 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerParser.kt
  16. +59 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/DeezerSession.kt
  17. +305 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/LocalAudioWebServer.kt
  18. +0 −16 ext/src/main/java/dev/brahmkshatriya/echo/extension/TestExtension.kt
  19. +243 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/Utils.kt
  20. +53 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerAlbum.kt
  21. +60 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerArtist.kt
  22. +100 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerMedia.kt
  23. +135 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerPlaylist.kt
  24. +56 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerRadio.kt
  25. +61 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerSearch.kt
  26. +54 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerTrack.kt
  27. +90 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/api/DeezerUtil.kt
  28. +66 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerAlbumClient.kt
  29. +100 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerArtistClient.kt
  30. +45 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerHomeFeedClient.kt
  31. +101 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLibraryClient.kt
  32. +40 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerLyricsClient.kt
  33. +61 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerPlaylistClient.kt
  34. +203 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerRadioClient.kt
  35. +150 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerSearchClient.kt
  36. +152 −0 ext/src/main/java/dev/brahmkshatriya/echo/extension/clients/DeezerTrackClient.kt
  37. +0 −195 ext/src/test/java/dev/brahmkshatriya/echo/extension/ExtensionUnitTest.kt
  38. +0 −22 ext/src/test/java/dev/brahmkshatriya/echo/extension/MockedSettings.kt
  39. +10 −11 gradle.properties
  40. +1 −1 gradle/wrapper/gradle-wrapper.properties
  41. +1 −1 settings.gradle.kts
18 changes: 16 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -19,15 +19,18 @@ jobs:
java-version: 17
cache: 'gradle'

- name: Cook Env
- name: Cook Variables from gradle.properties
run: |
name=$(grep '^extName=' gradle.properties | cut -d'=' -f2)
echo "NAME=$name Extension" >> $GITHUB_ENV
id=$(grep '^extId=' gradle.properties | cut -d'=' -f2)
echo "TAG=$id" >> $GITHUB_ENV
echo -e "## ${{ env.NAME }}\n${{ github.event.head_commit.message }}" > commit.txt
- name: Make Environment
run: |
version=$( echo ${{ github.event.head_commit.id }} | cut -c1-7 )
echo "VERSION=v$version" >> $GITHUB_ENV
echo -e "## ${{ env.NAME }}\n${{ github.event.head_commit.message }}" > commit.txt
echo "APP_PATH=app/build/${{ env.TAG }}-$version.eapk" >> $GITHUB_ENV
echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > $GITHUB_WORKSPACE/signing-key.jks
chmod +x ./gradlew
@@ -64,3 +67,14 @@ jobs:
release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload APK to Discord
shell: bash
env:
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: |
commit=$(jq -Rsa . <<< "${{ github.event.head_commit.message }}" | tail -c +2 | head -c -2)
message=$(echo "@everyone **${{ env.VERSION }}**\n$commit")
curl -F "payload_json={\"content\":\"${message}\"}" \
-F "file=@${{ env.APP_PATH }}" \
${{ env.WEBHOOK }}
141 changes: 67 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,67 @@
# Echo Extension Template

This is a template for creating an Echo extension. It includes a basic structure for the extension,
so you do not have to start from scratch.

## Getting Started

### 1. you can clone this repository.
Clone this repository and name it as you want.

### 2. Configure the [gradle.properties](gradle.properties)
The file will have the following properties:
- `libVersion` - The version of the Echo library, defaults to `main-SNAPSHOT`.
- `extType` - The type of the extension you want to create. It can be `music`, `tracker`
or `lyrics`. More information can be found
in [`Extension<*>`](https://github.com/brahmkshatriya/echo/blob/main/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt#L33-L43)
java doc.
- `extId` - The id of the extension. (Do not use spaces or special characters)
- `extClass` - The class of the extension. This should be the class that you inherit client
interfaces to. For example in this template, it
is [`TestExtension`](ext/src/main/java/dev/brahmkshatriya/echo/extension/TestExtension.kt).
- `extIcon` - (Optional) The icon of the extension. Will be cropped into a circle.
- `extName` - The name of the extension.
- `extDescription` - The description of the extension.
- `extAuthor` - The author of the extension.
- `extAuthorUrl` - (Optional) The author's website.
- `extRepoUrl` - (Optional) The repository URL of the extension.
- `extUpdateUrl` - (Optional) The update URL of the extension. The following urls are supported:
- Github : https://api.github.com/repos/your_username/your_extension_repo/releases

### 3. Implement the extension
Here's where the fun begins. Echo checks for `Client` interfaces that your extension implemented to know if your extension supports the feature or not.

- What are `Client` interfaces?
- These are interfaces that include functions your extension need to implement (`override fun`).
- For example, if you want to create a lyrics extension, you need to implement the `LyricsClient` interface.
- What interfaces are available?
- By default, the [`TestExtension`](ext/src/main/java/dev/brahmkshatriya/echo/extension/TestExtension.kt) implements the `ExtensionClient` interface.
- Pro tip: Hover over the interface to see the documentation, Click on every one things that is clickable to dive deep into the rabbit hole.
- You can find all the available interfaces, for:
- Music Extension - [here](https://github.com/brahmkshatriya/echo/blob/main/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt#L65-L117)
- Tracker Extension - [here](https://github.com/brahmkshatriya/echo/blob/main/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt#L123-L137)
- Lyrics Extension - [here](https://github.com/brahmkshatriya/echo/blob/main/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt#L143-L156)

The best example of how to implement an extension should be the [Spotify Extension](https://github.com/brahmkshatriya/echo-spotify-extension/blob/main/ext/src/main/java/dev/brahmkshatriya/echo/extension/SpotifyExtension.kt).

### 4. Making network requests
If your extension needs to make network requests, you can use `OkHttpClient` class provided directly by Echo. For example:
```kotlin
class TestExtension : ExtensionClient {
private val client = OkHttpClient()

override suspend fun someNiceFunction() {
val request = Request.Builder().url("https://example.com").build()
val response = client.newCall(request).await()
println(response.body?.string())
}
}
```
If you are using `OkHttpClient`, use the custom `await()` function for suspending the network call.

### 5. Testing the extension
There are two ways to test the extension:
- **Local testing**: You can test the extension locally by running the tests in the [`ExtensionUnitTest`](ext/src/test/java/dev/brahmkshatriya/echo/extension/ExtensionUnitTest.kt) class.
- **App testing**: You can test the extension in the Echo app by building & installing the `app` & then opening Echo app.

### 6. Publishing the extension
This template includes a GitHub Actions workflow that will automatically build and publish the extension to GitHub releases when you make a new commit. You can find the workflow file [here](.github/workflow/build.yml).
You need to do the following steps to publish the extension:
- Enable `Read & write permissions` for workflows in the repository settings (Settings -> Actions -> General -> Workflow Permissions).
- Generate a keystore file : https://developer.android.com/studio/publish/app-signing#generate-key
- Add action secrets in the repository settings (Settings -> Secrets and variables -> Actions -> New repository secret):
- `KEYSTORE_B64` - The base64 encoded keystore file. [How to](https://stackoverflow.com/a/70396534)
- `PASSWORD` - The password of the keystore file.
[![Latest Version](https://img.shields.io/github/v/release/LuftVerbot/echo-deezer-extension?color=blue)](https://github.com/LuftVerbot/echo-deezer-extension/releases/latest)
# Deezer Extension for Echo

This is an extension for the Extension based Music Player [Echo](https://github.com/brahmkshatriya/echo). It aims to implement a seamless Deezer experience without needing a premium account.

## Features

- **High-Quality Audio Support**: FLAC, 320kbps, or 128kbps without a premium account.
- **Multiple Login Options**: Choose from three different ways to sign in.
- **Personal Library Access**: Access your Deezer library directly within Echo.
- **Diverse Radio Options**: Explore various radio features, including Flow and more.

## Screenshots

<div align="center" style="text-align: center;">
<table>
<tr>
<td align="center" width="260px">
<img src="https://github.com/user-attachments/assets/58df2ca3-0bce-4ed8-96c6-fc6efc2ee9c6" alt="Home Screen" width="250"/><br>
🎵 Home Screen<br>
<span>All your music essentials in one place.</span>
</td>
<td align="center" width="260px">
<img src="https://github.com/user-attachments/assets/684c0492-807f-4886-811d-0cdd9dd2d513" alt="Search & Discover" width="250"/><br>
🔍 Search & Discover<br>
<span>Find and explore music just like on Deezer.</span>
</td>
<td align="center" width="260px">
<img src="https://github.com/user-attachments/assets/63b55a3b-725d-4f7e-b28f-99a9bd873345" alt="Your Deezer Library" width="250"/><br>
❤️ Your Deezer Library<br>
<span>Access and manage your favorite tracks within Echo.</span>
</td>
</tr>
</table>
</div>

## Installation

Ensure you have Echo installed on your device before proceeding. Currently it's only possible to get it from [Discord](https://discord.gg/J3WvbBUU8Z).

1. **Download the Extension**:
- Visit the [latest release page](https://github.com/LuftVerbot/echo-deezer-extension/releases/latest).
- Download the latest `.eapk` file of the Deezer Extension.

2. **Install the Extension**:
- Open the downloaded `.eapk` file.
- When prompted, select **Echo** as the application to install the extension.
- Follow the on-screen instructions to complete the installation.

3. **Launch Echo**:
- Open the Echo app.
- The Deezer Extension is now installed and ready to use.

## FAQ

### Why?

> *"I always wanted to create a successor of Freezer that could follow its footsteps, and Echo gave me a good opportunity to start with it."*
### How is FLAC/320 possible without a Premium Account?

> *"Well, I use a script, or rather a server, that gives back the premium songs. It was created by [@uhwot](https://github.com/uhwot), and I simply stumbled upon it while doing research about stuff for Deezer.*"
## Acknowledgements

- **[Echo](https://github.com/brahmkshatriya/echo)**: Created by [@brahmkshatriya](https://github.com/brahmkshatriya).
- **[dzunlock](https://uhwotgit.fly.dev/uhwot/dzunlock)**: Created by [@uhwot](https://github.com/uhwot).
4 changes: 3 additions & 1 deletion app/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/build
/build
/release
/debug
27 changes: 26 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -20,4 +20,29 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile

-keep class dev.brahmkshatriya.echo.** { *; }
-keep class dev.brahmkshatriya.echo.** { *; }
-dontwarn org.slf4j.impl.StaticLoggerBinder

-dontwarn okhttp3.Call
-dontwarn okhttp3.ConnectionPool
-dontwarn okhttp3.Headers$Builder
-dontwarn okhttp3.Headers$Companion
-dontwarn okhttp3.Headers
-dontwarn okhttp3.HttpUrl$Builder
-dontwarn okhttp3.HttpUrl$Companion
-dontwarn okhttp3.HttpUrl
-dontwarn okhttp3.Interceptor$Chain
-dontwarn okhttp3.Interceptor
-dontwarn okhttp3.MediaType$Companion
-dontwarn okhttp3.MediaType
-dontwarn okhttp3.OkHttpClient$Builder
-dontwarn okhttp3.OkHttpClient
-dontwarn okhttp3.Protocol
-dontwarn okhttp3.Request$Builder
-dontwarn okhttp3.Request
-dontwarn okhttp3.RequestBody$Companion
-dontwarn okhttp3.RequestBody
-dontwarn okhttp3.Response$Builder
-dontwarn okhttp3.Response
-dontwarn okhttp3.ResponseBody$Companion
-dontwarn okhttp3.ResponseBody
17 changes: 17 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -2,8 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature android:name="${type}" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:theme="@style/AppTheme"
android:icon="@mipmap/ic_launcher"
android:label="${app_name}">
<meta-data
@@ -43,5 +45,20 @@
<meta-data
android:name="update_url"
android:value="${update_url}" />
<activity
android:name="dev.brahmkshatriya.echo.link.Opener"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="www.deezer.com" />
<data android:host="deezer.page.link" />
</intent-filter>
</activity>
</application>
</manifest>
112 changes: 112 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/link/Opener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package dev.brahmkshatriya.echo.link

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

class Opener : Activity() {

private val extensionId = "deezerApp"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent.data

if (uri != null) {
Thread {
val resolvedUri = resolveRedirects(uri.toString())
runOnUiThread {
if (resolvedUri != null) {
processUri(Uri.parse(resolvedUri))
} else {
finishAndRemoveTask()
}
}
}.start()
}
}

private fun resolveRedirects(urlString: String): String? {
var url = urlString
var redirectUrl: String?

try {
while (true) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.instanceFollowRedirects = false
connection.connect()

val responseCode = connection.responseCode
if (responseCode in 300..399) {
redirectUrl = connection.getHeaderField("Location")
if (redirectUrl != null) {
url = if (Uri.parse(redirectUrl).isRelative) {
URL(URL(url), redirectUrl).toString()
} else {
redirectUrl
}
} else {
break
}
} else {
redirectUrl = url
break
}
connection.disconnect()
}
} catch (e: IOException) {
e.printStackTrace()
return null
}

return redirectUrl
}

private fun processUri(uri: Uri) {
val type: String
val segment: Int
if (!uri.pathSegments[0].contains("album") ||
!uri.pathSegments[0].contains("artist") ||
!uri.pathSegments[0].contains("playlist") ||
!uri.pathSegments[0].contains("track")
) {
type = uri.pathSegments[1]
segment = 2
} else {
type = uri.pathSegments[0]
segment = 1
}

val path = when (type) {
"artist" -> {
val artistId = uri.pathSegments[segment] ?: return
"artist/$artistId"
}

"playlist" -> {
val playlistId = uri.pathSegments[segment] ?: return
"playlist/$playlistId"
}

"album" -> {
val albumId = uri.pathSegments[segment] ?: return
"album/$albumId"
}

"track" -> {
val trackId = uri.pathSegments[segment] ?: return
"track/$trackId"
}

else -> return
}

val uriString = "echo://music/$extensionId/$path"
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uriString)))
finishAndRemoveTask()
}
}
Loading