From beff72b3804013fca59b543fd14e18ec94cb2c4e Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 5 Aug 2022 21:28:22 +0200 Subject: [PATCH] First commit --- .gitignore | 35 + app/.gitignore | 1 + app/build.gradle | 56 ++ app/proguard-rules.pro | 21 + .../downloader/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 43 ++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 19051 bytes .../main/java/app/deemix/downloader/Deezer.kt | 333 ++++++++++ .../deemix/downloader/DownloadItemAdapter.kt | 98 +++ .../app/deemix/downloader/DownloadWorker.kt | 369 +++++++++++ .../app/deemix/downloader/LoginActivity.kt | 105 +++ .../app/deemix/downloader/MainActivity.kt | 626 ++++++++++++++++++ .../app/deemix/downloader/SettingsActivity.kt | 77 +++ .../app/deemix/downloader/SharedObjects.kt | 10 + .../main/java/app/deemix/downloader/Utils.kt | 112 ++++ .../java/app/deemix/downloader/types/Album.kt | 188 ++++++ .../app/deemix/downloader/types/Artist.kt | 55 ++ .../app/deemix/downloader/types/DeezerDate.kt | 25 + .../deemix/downloader/types/DeezerPicture.kt | 35 + .../app/deemix/downloader/types/DeezerUser.kt | 56 ++ .../deemix/downloader/types/DownloadItem.kt | 141 ++++ .../app/deemix/downloader/types/Lyrics.kt | 28 + .../java/app/deemix/downloader/types/Track.kt | 263 ++++++++ .../drawable-v24/ic_launcher_foreground.xml | 44 ++ app/src/main/res/drawable/divider.xml | 9 + .../ic_baseline_arrow_downward_24.xml | 10 + .../res/drawable/ic_baseline_delete_24.xml | 10 + .../main/res/drawable/ic_baseline_done_24.xml | 10 + .../res/drawable/ic_baseline_equalizer_24.xml | 10 + .../res/drawable/ic_baseline_error_24.xml | 10 + .../drawable/ic_baseline_error_outline_24.xml | 10 + .../res/drawable/ic_baseline_get_app_24.xml | 10 + .../main/res/drawable/ic_baseline_link_24.xml | 10 + .../main/res/drawable/ic_baseline_list_24.xml | 10 + .../drawable/ic_baseline_music_note_24.xml | 10 + .../res/drawable/ic_baseline_settings_24.xml | 10 + .../res/drawable/ic_baseline_warning_24.xml | 10 + app/src/main/res/drawable/ic_deezer_logo.xml | 164 +++++ .../res/drawable/ic_launcher_background.xml | 10 + app/src/main/res/drawable/no_cover.xml | 18 + app/src/main/res/drawable/search_input.xml | 18 + app/src/main/res/drawable/tag_background.xml | 9 + app/src/main/res/layout/activity_main.xml | 79 +++ app/src/main/res/layout/download_item.xml | 130 ++++ app/src/main/res/layout/login_activity.xml | 88 +++ app/src/main/res/layout/settings_activity.xml | 72 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2470 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4423 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1561 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2469 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3407 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5953 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5126 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9858 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7346 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13271 bytes app/src/main/res/values-night/colors.xml | 12 + app/src/main/res/values-night/themes.xml | 18 + app/src/main/res/values/arrays.xml | 14 + app/src/main/res/values/colors.xml | 12 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 25 + app/src/main/res/values/themes.xml | 17 + app/src/main/res/xml/preferences.xml | 15 + .../app/deemix/downloader/ExampleUnitTest.kt | 17 + build.gradle | 10 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++ gradlew.bat | 89 +++ settings.gradle | 16 + 74 files changed, 3935 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/app/deemix/downloader/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/app/deemix/downloader/Deezer.kt create mode 100644 app/src/main/java/app/deemix/downloader/DownloadItemAdapter.kt create mode 100644 app/src/main/java/app/deemix/downloader/DownloadWorker.kt create mode 100644 app/src/main/java/app/deemix/downloader/LoginActivity.kt create mode 100644 app/src/main/java/app/deemix/downloader/MainActivity.kt create mode 100644 app/src/main/java/app/deemix/downloader/SettingsActivity.kt create mode 100644 app/src/main/java/app/deemix/downloader/SharedObjects.kt create mode 100644 app/src/main/java/app/deemix/downloader/Utils.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/Album.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/Artist.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/DeezerDate.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/DeezerPicture.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/DeezerUser.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/DownloadItem.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/Lyrics.kt create mode 100644 app/src/main/java/app/deemix/downloader/types/Track.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/divider.xml create mode 100644 app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_delete_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_done_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_equalizer_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_error_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_error_outline_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_get_app_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_link_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_list_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_music_note_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_settings_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_warning_24.xml create mode 100644 app/src/main/res/drawable/ic_deezer_logo.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/no_cover.xml create mode 100644 app/src/main/res/drawable/search_input.xml create mode 100644 app/src/main/res/drawable/tag_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/download_item.xml create mode 100644 app/src/main/res/layout/login_activity.xml create mode 100644 app/src/main/res/layout/settings_activity.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/preferences.xml create mode 100644 app/src/test/java/app/deemix/downloader/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..101a73e --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +.DS_Store + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..e8d6302 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "app.deemix.downloader" + minSdk 21 + targetSdk 32 + versionCode 1 + versionName "0.07" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding true + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' + implementation 'androidx.work:work-runtime-ktx:2.7.1' + implementation 'androidx.work:work-multiprocess:2.7.1' + implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' + implementation 'androidx.annotation:annotation:1.2.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' + implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'net.jthink:jaudiotagger:3.0.1' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/app/deemix/downloader/ExampleInstrumentedTest.kt b/app/src/androidTest/java/app/deemix/downloader/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3bdd42b --- /dev/null +++ b/app/src/androidTest/java/app/deemix/downloader/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package app.deemix.downloader + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("app.deemix.downloader", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a176833 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..1c63a5dd2509b2957da4ee0c4c0ff62d755baf99 GIT binary patch literal 19051 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/app/deemix/downloader/Deezer.kt b/app/src/main/java/app/deemix/downloader/Deezer.kt new file mode 100644 index 0000000..dface49 --- /dev/null +++ b/app/src/main/java/app/deemix/downloader/Deezer.kt @@ -0,0 +1,333 @@ +package app.deemix.downloader + +import android.os.Build +import app.deemix.downloader.types.DeezerUser +import org.json.JSONObject +import org.json.JSONTokener +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.Exception +import java.net.* +import javax.net.ssl.HttpsURLConnection + +class Deezer { + private var cookieJar: CookieManager = CookieManager() + private var httpHeaders: MutableMap = HashMap() + + var isLoggedIn = false + var currentUser: DeezerUser? = null + private var childs: ArrayList = ArrayList() + private var selectedAccount: Int = 0 + + private var apiToken: String? = null + + init { + httpHeaders["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36" + } + + fun apiCall(method: String): JSONObject { + var result: JSONObject + val request = URL("https://api.deezer.com/$method") + with(request.openConnection() as HttpsURLConnection) { + requestMethod = "GET" + doInput = true + + // Set Headers + for (key in httpHeaders.keys){ + setRequestProperty(key, httpHeaders[key]) + } + + // Set Cookies + if (cookieJar.cookieStore.cookies.size > 0) { + var cookieText = "" + for (cookie in cookieJar.cookieStore.cookies) { + cookieText += "${cookie.name}=${cookie.value}; " + } + setRequestProperty("Cookie", cookieText) + } + + // Save set cookies + val thisHeaderFields: Map> = headerFields + val cookiesHeader = thisHeaderFields["Set-Cookie"] + if (cookiesHeader != null) { + for (cookie in cookiesHeader) { + val thisCookie = HttpCookie.parse(cookie) + cookieJar.cookieStore.add(URI("https://www.deezer.com"), thisCookie[0]) + } + } + + BufferedReader(InputStreamReader(inputStream)).use { + result = JSONTokener(it.readText()).nextValue() as JSONObject + } + } + if (result.has("error")){ + val error = result.getJSONObject("error") + if (error.has("code")){ + val errorCode = error.getInt("code") + if (arrayOf(4, 700).contains(errorCode)){ + return apiCall(method) + } + } + throw Exception(error.toString(0).replace("\n", "")) + } + return result + } + + fun apiCallGW(method: String, args:String? = null): JSONObject{ + if (apiToken == null && method != "deezer.getUserData") apiToken = getToken() + var result: JSONObject + + val request = URL("https://www.deezer.com/ajax/gw-light.php?api_version=1.0&api_token=${ if (method == "deezer.getUserData") "null" else apiToken }&input=3&method=$method") + with(request.openConnection() as HttpsURLConnection) { + requestMethod = "POST" + doInput = true + doOutput = args != null + + // Set Headers + for (key in httpHeaders.keys){ + setRequestProperty(key, httpHeaders[key]) + } + + // Set Cookies + if (cookieJar.cookieStore.cookies.size > 0) { + var cookieText = "" + for (cookie in cookieJar.cookieStore.cookies) { + cookieText += "${cookie.name}=${cookie.value}; " + } + setRequestProperty("Cookie", cookieText) + } + + // Set post body + if (args != null){ + setRequestProperty("Content-Type", "application/json; charset=utf-8") + outputStream.write(args.toByteArray()) + outputStream.flush() + } + + // Save set cookies + val thisHeaderFields: Map> = headerFields + val cookiesHeader = thisHeaderFields["Set-Cookie"] + if (cookiesHeader != null) { + for (cookie in cookiesHeader) { + val thisCookie = HttpCookie.parse(cookie) + cookieJar.cookieStore.add(URI("https://www.deezer.com"), thisCookie[0]) + } + } + + BufferedReader(InputStreamReader(inputStream)).use { + result = JSONTokener(it.readText()).nextValue() as JSONObject + } + } + if (result.has("error") && result.get("error").toString() != "[]"){ + val error = result.getJSONObject("error").toString() + if ( + !(error != "{\"GATEWAY_ERROR\":\"invalid api token\"}" && error != "{\"VALID_TOKEN_REQUIRED\":\"Invalid CSRF token\"}") + ){ + apiToken = getToken() + return apiCallGW(method, args) + } + if (result.has("payload") && result.getJSONObject("payload").has("FALLBACK")){ + val thisArgs = JSONObject(args) + val fallback = result.getJSONObject("payload").getJSONObject("FALLBACK") + for (key in fallback.keys()){ + thisArgs.put(key, fallback.get(key)) + } + return apiCallGW(method, thisArgs.toString(0).replace("\n", "")) + } + throw Exception(result.getJSONObject("error").toString(0).replace("\n", "")) + } + val unknownResult = result.get("results") + if (unknownResult is JSONObject) result = unknownResult + else { + result = JSONObject() + result.put("data", unknownResult) + } + if (apiToken == null && method == "deezer.getUserData") apiToken = result.getString("checkForm") + return result + } + + private fun getToken(): String{ + val tokenData = apiCallGW("deezer.getUserData") + return tokenData.getString("checkForm") + } + + fun getAccessToken(email: String, password: String): String?{ + val clientId = "172365" + val clientSecret = "fb0bec7ccc063dab0417eb7b0d847f34" + + var accessToken: String? = null + val hashedPassword = password.toMD5() + val hash = "$clientId$email$hashedPassword$clientSecret".toMD5() + var response: JSONObject + val request = URL("https://api.deezer.com/auth/token?app_id=$clientId&login=$email&password=$hashedPassword&hash=$hash") + with(request.openConnection() as HttpsURLConnection) { + setRequestProperty("User-Agent", httpHeaders["User-Agent"]) + + BufferedReader(InputStreamReader(inputStream)).use { + response = JSONTokener(it.readText()).nextValue() as JSONObject + } + } + + if (response.has("access_token")) accessToken = response.getString("access_token") + if (accessToken == null) logout() + return accessToken + } + + fun getArlFromAccessToken(accessToken: String): String? { + val request = URL("https://api.deezer.com/platform/generic/track/3135556") + with(request.openConnection() as HttpsURLConnection) { + setRequestProperty("User-Agent", httpHeaders["User-Agent"]) + setRequestProperty("Authorization", "Bearer $accessToken") + + // Save set Cookies + val thisHeaderFields: Map> = headerFields + val cookiesHeader = thisHeaderFields["Set-Cookie"] + if (cookiesHeader != null) { + for (cookie in cookiesHeader) { + val thisCookie = HttpCookie.parse(cookie) + cookieJar.cookieStore.add(URI("https://www.deezer.com"), thisCookie[0]) + } + } + + } + return apiCallGW("user.getArl").getString("data") + } + + fun login(arl: String): Boolean{ + val thisArl = arl.trim() + val arlCookie = HttpCookie("arl", thisArl) + arlCookie.domain = ".deezer.com" + arlCookie.path = "/" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + arlCookie.isHttpOnly = true + } + cookieJar.cookieStore.add(URI("https://www.deezer.com"), arlCookie) + + var userData = apiCallGW("deezer.getUserData") + if (userData.length() == 0) { + isLoggedIn = false + return isLoggedIn + } + userData = userData.getJSONObject("USER") + if (userData.getInt("USER_ID") == 0){ + println("USER_ID = 0") + println(cookieJar.cookieStore.cookies) + isLoggedIn = false + return isLoggedIn + } + + postLogin(userData) + changeAccount(0) + isLoggedIn = true + return isLoggedIn + } + + fun loginViaEmail(email: String, password: String){ + val accessToken = SharedObjects.dz.getAccessToken(email, password) ?: return + val arl = SharedObjects.dz.getArlFromAccessToken(accessToken) ?: return + login(arl) + } + + private fun changeAccount(childPos: Int): Pair { + var thisChildPos = childPos + if (childs.size-1 < thisChildPos) thisChildPos = 0 + currentUser = childs[thisChildPos] + selectedAccount = thisChildPos + httpHeaders["Accept-Language"] = currentUser!!.language + + return Pair(currentUser!!, thisChildPos) + } + + private fun postLogin(userData: JSONObject) { + childs.clear() + val isFamily = userData.getJSONObject("MULTI_ACCOUNT").getBoolean("ENABLED") && ! userData.getJSONObject("MULTI_ACCOUNT").getBoolean("IS_SUB_ACCOUNT") + if (isFamily){ + val deezerChilds = apiCallGW("deezer.getChildAccounts") + for (i in 0 until deezerChilds.length()){ + val child = deezerChilds.getJSONObject(i.toString()) + childs.add(DeezerUser(userData, child)) + } + } else { + childs.add(DeezerUser(userData)) + } + } + + fun logout() { + cookieJar = CookieManager() + httpHeaders["Accept-Language"] = "" + childs.clear() + currentUser = null + isLoggedIn = false + } + + fun getTrackURL(trackToken: String, format: String): String?{ + if (!isLoggedIn) return null + if (format == "MP4_RA" && !currentUser!!.canStream.reality) return null + if (format == "FLAC" && !currentUser!!.canStream.lossless) return null + if (format == "MP3_320" && !currentUser!!.canStream.high) return null + if (format == "MP3_128" && !currentUser!!.canStream.standard) return null + + var result: JSONObject + val request = URL("https://media.deezer.com/v1/get_url") + with(request.openConnection() as HttpsURLConnection) { + requestMethod = "POST" + doInput = true + doOutput = true + + // Set Headers + for (key in httpHeaders.keys){ + setRequestProperty(key, httpHeaders[key]) + } + + // Set Cookies + if (cookieJar.cookieStore.cookies.size > 0) { + var cookieText = "" + for (cookie in cookieJar.cookieStore.cookies) { + cookieText += "${cookie.name}=${cookie.value}; " + } + setRequestProperty("Cookie", cookieText) + } + + // Set post body + setRequestProperty("Content-Type", "application/json; charset=utf-8") + outputStream.write(""" + { + "license_token": "${currentUser!!.licenseToken}", + "media": [{"type": "FULL", "formats": [{ "cipher": "BF_CBC_STRIPE", "format": "$format"}]}], + "track_tokens": ["$trackToken"] + } + """.trimIndent().toByteArray()) + outputStream.flush() + + // Save set cookies + val thisHeaderFields: Map> = headerFields + val cookiesHeader = thisHeaderFields["Set-Cookie"] + if (cookiesHeader != null) { + for (cookie in cookiesHeader) { + val thisCookie = HttpCookie.parse(cookie) + cookieJar.cookieStore.add(URI("https://www.deezer.com"), thisCookie[0]) + } + } + + BufferedReader(InputStreamReader(inputStream)).use { + result = JSONTokener(it.readText()).nextValue() as JSONObject + } + } + + if (result.has("data")){ + for (i in 0 until result.getJSONArray("data").length()){ + val data: JSONObject = result.getJSONArray("data").getJSONObject(i) + if (data.has("errors")){ + println(data.get("errors").toString()) + return null + } + return if (data.has("media") && data.getJSONArray("media").length() > 0){ + data.getJSONArray("media").getJSONObject(0).getJSONArray("sources").getJSONObject(0).getString("url") + } else { + null + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/app/deemix/downloader/DownloadItemAdapter.kt b/app/src/main/java/app/deemix/downloader/DownloadItemAdapter.kt new file mode 100644 index 0000000..5076055 --- /dev/null +++ b/app/src/main/java/app/deemix/downloader/DownloadItemAdapter.kt @@ -0,0 +1,98 @@ +package app.deemix.downloader + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import app.deemix.downloader.types.DownloadItem +import com.squareup.picasso.Picasso + +class DownloadItemAdapter(private val dataSet: Map, private val order: ArrayList) : + RecyclerView.Adapter() { + + class DownloadViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val title: TextView = view.findViewById(R.id.downloadTitle) + val artist: TextView = view.findViewById(R.id.downloadArtist) + val progress: TextView = view.findViewById(R.id.downloadProgress) + val quality: TextView = view.findViewById(R.id.qualityLabel) + val fails: TextView = view.findViewById(R.id.downloadFails) + val bar: ProgressBar = view.findViewById(R.id.downloadBar) + val cover: ImageView = view.findViewById(R.id.coverImage) + val status: ImageView = view.findViewById(R.id.downloadStatus) + + private fun statusIcon(status: String): Int{ + return when (status){ + "inQueue" -> R.drawable.ic_baseline_list_24 + "downloading" -> R.drawable.ic_baseline_arrow_downward_24 + "downloaded" -> R.drawable.ic_baseline_done_24 + "downloadedWithErrors" -> R.drawable.ic_baseline_warning_24 + "failed" -> R.drawable.ic_baseline_error_24 + else -> R.drawable.ic_baseline_list_24 + } + } + + fun updateDownloadItem(downloadItem: DownloadItem){ + progress.text = "${downloadItem.downloaded+downloadItem.failed}/${downloadItem.size}" + status.setImageResource(statusIcon(downloadItem.status)) + if (downloadItem.failed > 0){ + fails.text = "${downloadItem.failed} (!)" + fails.visibility = View.VISIBLE + } else { + fails.visibility = View.GONE + } + when { + downloadItem.progress == -1 -> { + bar.isIndeterminate = true + } + downloadItem.progress >= 100 -> { + bar.isIndeterminate = false + bar.progress = 100 + } + else -> { + bar.isIndeterminate = false + bar.progress = downloadItem.progress + } + } + } + } + + // Create new views (invoked by the layout manager) + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): DownloadViewHolder { + // Create a new view, which defines the UI of the list item + val view = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.download_item, viewGroup, false) + + return DownloadViewHolder(view) + } + + private fun qualityText(quality: String): String{ + return when (quality) { + "9" -> "FLAC" + "3" -> "320" + "1" -> "128" + else -> "MISC" + } + } + + // Replace the contents of a view (invoked by the layout manager) + override fun onBindViewHolder(downloadViewHolder: DownloadViewHolder, position: Int) { + val downloadItem = dataSet[order[position]]!! + + downloadViewHolder.title.text = downloadItem.title + downloadViewHolder.artist.text = downloadItem.artist + downloadViewHolder.quality.text = qualityText(downloadItem.bitrate) + Picasso.get() + .load(downloadItem.cover) + .placeholder(R.drawable.no_cover) + .error(R.drawable.no_cover) + .into(downloadViewHolder.cover) + downloadViewHolder.updateDownloadItem(downloadItem) + } + + // Return the size of your dataset (invoked by the layout manager) + override fun getItemCount() = order.size + +} \ No newline at end of file diff --git a/app/src/main/java/app/deemix/downloader/DownloadWorker.kt b/app/src/main/java/app/deemix/downloader/DownloadWorker.kt new file mode 100644 index 0000000..7327713 --- /dev/null +++ b/app/src/main/java/app/deemix/downloader/DownloadWorker.kt @@ -0,0 +1,369 @@ +package app.deemix.downloader + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.graphics.Color +import android.os.Build +import android.os.Environment +import android.support.annotation.RequiresApi +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.PRIORITY_MIN +import androidx.work.* +import app.deemix.downloader.SharedObjects.dz +import app.deemix.downloader.SharedObjects.queue +import app.deemix.downloader.Utils.createBlowfishKey +import app.deemix.downloader.Utils.decryptBlowfish +import app.deemix.downloader.types.Album +import app.deemix.downloader.types.DownloadItem +import app.deemix.downloader.types.Track +import org.jaudiotagger.audio.AudioFileIO +import org.jaudiotagger.tag.FieldKey +import org.jaudiotagger.tag.images.ArtworkFactory +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.net.URL +import javax.net.ssl.HttpsURLConnection +import kotlin.math.roundToInt +import java.lang.Exception + +class DownloadWorker(private val appContext: Context, workerParams: WorkerParameters): + CoroutineWorker(appContext, workerParams) { + + companion object { + private var uuid = "" + private lateinit var currentItem: DownloadItem + + private var totalSize = 0 + private var progressNext: Double = 0.00 + private var bitrate: String = "0" + + private var title = "" + val NOTIFICATION_ID = 42 + lateinit var notification: Notification + } + + private fun createNotification(): Notification { + val channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel("my_service", "My Background Service") + } else { + // If earlier version channel ID is not used + // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context) + "" + } + + val notificationBuilder = NotificationCompat.Builder(appContext, channelId ) + return notificationBuilder.setOngoing(true) + .setSmallIcon(R.drawable.ic_baseline_get_app_24) + .setPriority(PRIORITY_MIN) + .setCategory(Notification.CATEGORY_SERVICE) + .setContentTitle(title) + .setTicker(title) + .build() + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createNotificationChannel(channelId: String, channelName: String): String{ + val chan = NotificationChannel(channelId, + channelName, NotificationManager.IMPORTANCE_NONE) + chan.lightColor = Color.BLUE + chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE + val service = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + service.createNotificationChannel(chan) + return channelId + } + + override suspend fun doWork(): Result { + // Get the UUID + val thisUuid = inputData.getString("uuid") + if (thisUuid == null){ + println("[DOWNLOAD ERROR] No UUID found") + return Result.failure() + } + + uuid = thisUuid + currentItem = queue[uuid] ?: return Result.success() + currentItem.progress = 0 + currentItem.downloaded = 0 + currentItem.failed = 0 + currentItem.errors = ArrayList() + currentItem.status = "downloading" + title = "${currentItem.artist} - ${currentItem.title}" + + // Get the data from disk + val downloadObject: JSONObject + try { + appContext.openFileInput("$uuid.json").use { stream -> + val text = stream.bufferedReader().use { + it.readText() + } + downloadObject = JSONObject(text) + } + } catch (e: Exception){ + println("[DOWNLOAD ERROR] Error: $e") + return Result.failure() + } + downloadObject.put("status", currentItem.status) + appContext.openFileOutput("$uuid.json", Context.MODE_PRIVATE).use { output -> + output.write(downloadObject.toString().toByteArray()) + } + notification = createNotification() + sendUpdate() + + // Get the downloadData + var downloadData: JSONObject? = null + val downloadType: String = downloadObject.getString("itemType") + if (downloadType == "single") downloadData = downloadObject.getJSONObject("single") + else if (downloadType == "collection") downloadData = downloadObject.getJSONObject("collection") + if (downloadData == null) { + println("[DOWNLOAD ERROR] No download Data") + return Result.failure() + } + totalSize = downloadObject.getInt("size") + progressNext = 0.00 + bitrate = downloadObject.getString("bitrate") + + // Start Download + if (downloadType == "single"){ + val track: Track = Track().restoreObject(downloadData.getJSONObject("trackAPI")) + val album: Album? = if (downloadData.has("albumAPI")) Album().restoreObject(downloadData.getJSONObject("albumAPI")) else null + downloadWrapper(track, album) + } else if (downloadType == "collection"){ + val album: Album? = if (downloadData.has("albumAPI")) Album().restoreObject(downloadData.getJSONObject("albumAPI")) else null + for (i in 0 until downloadData.getJSONArray("tracks").length()){ + val track: Track = Track().restoreObject(downloadData.getJSONArray("tracks").getJSONObject(i)) + downloadWrapper(track, album) + } + } + + currentItem.progress = 100 + downloadObject.put("progress", currentItem.progress) + downloadObject.put("downloaded", currentItem.downloaded) + downloadObject.put("failed", currentItem.failed) + downloadObject.put("errors", JSONArray(currentItem.errors)) + + currentItem.status = "downloaded" + if (currentItem.failed == currentItem.downloaded + currentItem.failed){ + currentItem.status = "failed" + } else if (currentItem.failed > 0){ + currentItem.status = "downloadedWithErrors" + } + downloadObject.put("status", currentItem.status) + sendUpdate() + + appContext.openFileOutput("$uuid.json", Context.MODE_PRIVATE).use { output -> + output.write(downloadObject.toString().toByteArray()) + } + return Result.success() + } + + private suspend fun sendUpdate(){ + if (isStopped) return + setProgress( Data.Builder() + .putString("uuid", uuid) + .putInt("progress", currentItem.progress) + .putInt("downloaded", currentItem.downloaded) + .putInt("failed", currentItem.failed) + .putStringArray("errors", currentItem.errors.toTypedArray()) + .putString("status", currentItem.status) + .build() + ) + setForeground(ForegroundInfo(NOTIFICATION_ID, notification)) + } + + private suspend fun download(track: Track, album: Album? = null){ + // Get Tags + val sngId = track.id + if (album != null) track.album = album + + if (track.isLocal){ + // Only to get refreshed track tokens + val trackGW = dz.apiCallGW("song.getData", "{\"SNG_ID\": $sngId}") + track.parseGW(trackGW) + } else { + val trackPageGW = dz.apiCallGW("deezer.pageTrack", "{\"SNG_ID\": $sngId}") + track.parseGWPage(trackPageGW) + + // Only standard API has track bpm + if (track.bpm == null){ + val trackAPI = dz.apiCall("track/$sngId") + if (trackAPI.has("bpm")) { + track.bpm = trackAPI.getString("bpm").toFloat() + } + } + // Only standard API has album genres + if (track.album.genres == null){ + try { + val albumAPI = dz.apiCall("album/${track.album.id}") + track.album.parseAPI(albumAPI) + } catch (_: Exception){} + } + // Only gw api has album discTotal + if (track.album.discTotal == null){ + try { + val albumAPI = dz.apiCallGW("album.getData", "{\"ALB_ID\": ${track.album.id}}") + track.album.parseGW(albumAPI) + } catch (_: Exception){} + } + + if (track.album.date != null && track.date == null) track.date = track.album.date + } + track.cleanUp() + + // Get the correct bitrate + val thisBitrate = bitrate + val extension = when (thisBitrate){ + "9" -> "flac" + else -> "mp3" + } + val format = when (thisBitrate){ + "9" -> "FLAC" + "3" -> "MP3_320" + "1" -> "MP3_128" + else -> "MP3_128" + } + // Apply settings + // Generate filename and filepath from metadata + val filePath = "${Environment.getExternalStorageDirectory().absolutePath}/Music/Deezer" + val fileName = "${track.artist.name} - ${track.title}" + val finalFilePath = "$filePath/$fileName.$extension" + // Create the download folder if it doesn't exists + File(filePath).mkdirs() + + // Generate cover URLs + val coverSize = 800 + track.album.embeddedCoverURL = track.album.pic.getURL(coverSize, "jpg-80") + // Download and cache the coverart + val coverTempFile = File("${appContext.cacheDir}/alb${track.album.id}_$coverSize.jpg") + if (!coverTempFile.exists()) { + val thisAlbumArt: ByteArray? = URL(track.album.embeddedCoverURL).readBytes() + if (thisAlbumArt != null && !coverTempFile.exists()) { + coverTempFile.createNewFile() + coverTempFile.writeBytes(thisAlbumArt) + } + } + track.album.embeddedCoverPath = coverTempFile.absolutePath + + // Download the track + val trackURL: String = dz.getTrackURL(track.trackToken!!, format) + ?: throw Exception("No URL FOUND") + val blowfishKey = createBlowfishKey(sngId) + + val request = URL(trackURL) + with(request.openConnection() as HttpsURLConnection) { + connect() + val outputFile = File(finalFilePath) + outputFile.createNewFile() + + var place = 0 + val chunk = ByteArray(16384) + + var modifiedStream = ByteArray(0) + while (place < contentLength){ + val size = inputStream.read(chunk) // Returns the actual size of the chunk + var cutChunk = chunk.copyOfRange(0, size) // Cuts down the chunk to the actual size + + modifiedStream += cutChunk + while (modifiedStream.size >= 2048 * 3){ + var decryptedChunk = ByteArray(0) + val decryptingChunk = modifiedStream.copyOfRange(0, 2048 * 3) + modifiedStream = modifiedStream.copyOfRange(2048 * 3, modifiedStream.size) + if (decryptingChunk.size > 2048){ + decryptedChunk = decryptBlowfish(decryptingChunk.copyOfRange(0, 2048), blowfishKey) + decryptedChunk += decryptingChunk.copyOfRange(2048, decryptingChunk.size) + } + outputFile.appendBytes(decryptedChunk) + } + + place += size + updateProgress(size, contentLength) + } + if (modifiedStream.size >= 2048) { + var decryptedChunk = decryptBlowfish(modifiedStream.copyOfRange(0, 2048), blowfishKey) + decryptedChunk += modifiedStream.copyOfRange(2048, modifiedStream.size) + outputFile.appendBytes(decryptedChunk) + } else { + outputFile.appendBytes(modifiedStream) + } + } + // Add tags to the track + if (!track.isLocal) + tagFile(finalFilePath, track) + } + + private fun tagFile(finalFilePath: String, track: Track) { + val f = AudioFileIO.read(File(finalFilePath)) + + val tag = f.createDefaultTag() + tag.setField(FieldKey.TITLE, track.title) + for (artist in track.artists){ + tag.addField(FieldKey.ARTIST, artist) + } + tag.setField(FieldKey.ALBUM, track.album.title) + for (artist in track.album.artists){ + tag.addField(FieldKey.ALBUM_ARTIST, artist) + } + + tag.setField(FieldKey.TRACK, track.trackNumber) + tag.setField(FieldKey.TRACK_TOTAL, track.album.trackTotal) + tag.setField(FieldKey.DISC_NO, track.discNumber) + tag.setField(FieldKey.TRACK_TOTAL, track.album.discTotal) + + + if (track.album.genres != null){ + for (genre in track.album.genres!!){ + tag.addField(FieldKey.GENRE, genre) + } + } + if (track.album.date != null){ + tag.setField(FieldKey.YEAR, track.album.date!!.toString()) + } + // TODO: Add TLEN + if (track.bpm != null && track.bpm != 0f) tag.setField(FieldKey.BPM, track.bpm.toString()) + if (!track.album.label.isNullOrEmpty()) tag.setField(FieldKey.RECORD_LABEL, track.album.label) + tag.setField(FieldKey.ISRC, track.isrc) + if (!track.album.barcode.isNullOrEmpty()) tag.setField(FieldKey.BARCODE, track.album.barcode) + + // TODO: Add explicit + // TODO: Add replaygain + // TODO: Add unsync lyrics USLT + // TODO: Add sync lyrics SYLT + // TODO: Add involved people and composer + + tag.setField(FieldKey.COPYRIGHT, track.album.copyright) + // TODO: Add compilation check IS_COMPILATION + + // TODO: Add source and sourceid + // TODO: Add rating + val artwork = ArtworkFactory.createArtworkFromFile(File(track.album.embeddedCoverPath)) + tag.setField(artwork) + + f.tag = tag + f.commit() + } + + private suspend fun updateProgress(chunkSize: Int, fileSize: Int){ + progressNext += ((chunkSize.toDouble() / fileSize.toDouble()) / totalSize.toDouble()) * 100.00 + + if (progressNext.roundToInt() != currentItem.progress && progressNext.roundToInt() % 2 == 0){ + currentItem.progress = progressNext.roundToInt() + sendUpdate() + } + } + + private suspend fun downloadWrapper(track: Track, album: Album? = null){ + try { + download(track, album) + currentItem.downloaded += 1 + } catch (e: Exception){ + currentItem.failed += 1 + currentItem.errors.add(e.toString()) + println(e) + Log.e("deemix", "DownloadeError: ", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/deemix/downloader/LoginActivity.kt b/app/src/main/java/app/deemix/downloader/LoginActivity.kt new file mode 100644 index 0000000..35b6662 --- /dev/null +++ b/app/src/main/java/app/deemix/downloader/LoginActivity.kt @@ -0,0 +1,105 @@ +package app.deemix.downloader + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.EditText +import android.widget.ProgressBar +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import app.deemix.downloader.SharedObjects.dz +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + + +class LoginActivity : AppCompatActivity() { + + private lateinit var loginButton: Button + private lateinit var emailField: EditText + private lateinit var passwordField: EditText + private lateinit var loading: ProgressBar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.login_activity) + + loginButton = findViewById