First commit
35
.gitignore
vendored
Normal file
|
@ -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
|
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
56
app/build.gradle
Normal file
|
@ -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'
|
||||||
|
}
|
21
app/proguard-rules.pro
vendored
Normal file
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
43
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="app.deemix.downloader">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.DeezerDownloader">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".SettingsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_settings" />
|
||||||
|
<activity
|
||||||
|
android:name=".LoginActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_login" />
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
BIN
app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 19 KiB |
333
app/src/main/java/app/deemix/downloader/Deezer.kt
Normal file
|
@ -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<String, String> = HashMap()
|
||||||
|
|
||||||
|
var isLoggedIn = false
|
||||||
|
var currentUser: DeezerUser? = null
|
||||||
|
private var childs: ArrayList<DeezerUser> = 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<String, List<String>> = 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<String, List<String>> = 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<String, List<String>> = 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<DeezerUser, Int> {
|
||||||
|
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<String, List<String>> = 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, DownloadItem>, private val order: ArrayList<String>) :
|
||||||
|
RecyclerView.Adapter<DownloadItemAdapter.DownloadViewHolder>() {
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
369
app/src/main/java/app/deemix/downloader/DownloadWorker.kt
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
app/src/main/java/app/deemix/downloader/LoginActivity.kt
Normal file
|
@ -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<Button>(R.id.loginButton)
|
||||||
|
emailField = findViewById<EditText>(R.id.emailField)
|
||||||
|
passwordField = findViewById<EditText>(R.id.passwordField)
|
||||||
|
loading = findViewById<ProgressBar>(R.id.loginLoading)
|
||||||
|
|
||||||
|
loginButton.setOnClickListener {
|
||||||
|
if (!emailField.text.isNullOrBlank() && !passwordField.text.isNullOrBlank()){
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
toggleLogin(false)
|
||||||
|
val email = emailField.text.toString()
|
||||||
|
val password = passwordField.text.toString()
|
||||||
|
|
||||||
|
val accessToken = dz.getAccessToken(email, password)
|
||||||
|
if (accessToken == null) {
|
||||||
|
showToast("Couldn't retrieve accessToken")
|
||||||
|
toggleLogin(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val arl = dz.getArlFromAccessToken(accessToken)
|
||||||
|
if (arl == null) {
|
||||||
|
showToast("Couldn't retrieve arl")
|
||||||
|
toggleLogin(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val loggedIn = dz.login(arl)
|
||||||
|
if (loggedIn){
|
||||||
|
saveLogin(accessToken, arl)
|
||||||
|
val thisIntent = Intent(this@LoginActivity, MainActivity::class.java)
|
||||||
|
thisIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain"){
|
||||||
|
thisIntent.action = Intent.ACTION_SEND
|
||||||
|
thisIntent.type = "text/plain"
|
||||||
|
thisIntent.putExtra(Intent.EXTRA_TEXT, intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||||
|
}
|
||||||
|
this@LoginActivity.startActivity(thisIntent)
|
||||||
|
this@LoginActivity.finish()
|
||||||
|
} else {
|
||||||
|
showToast("Couldn't login")
|
||||||
|
toggleLogin(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleLogin(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLogin(accessToken: String, arl: String) {
|
||||||
|
openFileOutput("login", Context.MODE_PRIVATE).use { output ->
|
||||||
|
output.write("$accessToken\n$arl".toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleLogin(value: Boolean){
|
||||||
|
runOnUiThread {
|
||||||
|
if (value) {
|
||||||
|
loginButton.isEnabled = true
|
||||||
|
loading.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
loginButton.isEnabled = false
|
||||||
|
loading.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showToast(text: String){
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this, text,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
626
app/src/main/java/app/deemix/downloader/MainActivity.kt
Normal file
|
@ -0,0 +1,626 @@
|
||||||
|
package app.deemix.downloader
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.work.*
|
||||||
|
import app.deemix.downloader.SharedObjects.dz
|
||||||
|
import app.deemix.downloader.SharedObjects.fullQueue
|
||||||
|
import app.deemix.downloader.SharedObjects.queue
|
||||||
|
import app.deemix.downloader.Utils.getFinalURL
|
||||||
|
import app.deemix.downloader.types.*
|
||||||
|
import app.deemix.downloader.types.Collection
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.json.JSONTokener
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var adapter: DownloadItemAdapter
|
||||||
|
private var constraints: Constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).setRequiresStorageNotLow(true).build()
|
||||||
|
private val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
|
||||||
|
private var hasBeenInit: Boolean = false
|
||||||
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
println("onCreate")
|
||||||
|
// Set layout to main activity
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
// Get layout components
|
||||||
|
val downloadInputBox = findViewById<EditText>(R.id.downloadInputBox)
|
||||||
|
val downloadQueueView = findViewById<RecyclerView>(R.id.downloadQueue)
|
||||||
|
val settingsButton = findViewById<ImageButton>(R.id.settingsButton)
|
||||||
|
|
||||||
|
// Create the download queue list with the Recycler View
|
||||||
|
|
||||||
|
downloadQueueView.layoutManager = LinearLayoutManager(this)
|
||||||
|
val itemTouchHelperCallback = object :
|
||||||
|
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
val position = viewHolder.adapterPosition
|
||||||
|
val uuid = fullQueue[position]
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
removeFromQueue(uuid, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
|
||||||
|
itemTouchHelper.attachToRecyclerView(downloadQueueView)
|
||||||
|
adapter = DownloadItemAdapter(queue, fullQueue)
|
||||||
|
downloadQueueView.adapter = adapter
|
||||||
|
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
// Open settings when clicking the button
|
||||||
|
settingsButton.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add deezer links to queue on downloadInputBox enter key press
|
||||||
|
downloadInputBox.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
var link = downloadInputBox.text.toString()
|
||||||
|
downloadInputBox.setText("") // Clear the text box
|
||||||
|
|
||||||
|
if (URLUtil.isValidUrl(link) and link.contains("deezer")){
|
||||||
|
link = link.replace("http://", "https://")
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
addUrlToQueue(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the keyboard
|
||||||
|
val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(v.windowToken, 0)
|
||||||
|
|
||||||
|
// Remove focus
|
||||||
|
downloadInputBox.clearFocus()
|
||||||
|
return@OnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Listener for the download queue
|
||||||
|
WorkManager.getInstance(this)
|
||||||
|
.getWorkInfosForUniqueWorkLiveData("downloadQueue")
|
||||||
|
.observe(this, Observer { listOfWorkInfo: List<WorkInfo>? ->
|
||||||
|
if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) return@Observer
|
||||||
|
var currentItem: WorkInfo? = null
|
||||||
|
for (workInfo in listOfWorkInfo){
|
||||||
|
if (workInfo.state == WorkInfo.State.RUNNING){
|
||||||
|
currentItem = workInfo
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentItem == null) return@Observer
|
||||||
|
|
||||||
|
val update = currentItem.progress
|
||||||
|
val uuid = update.getString("uuid")
|
||||||
|
val position = fullQueue.indexOf(uuid)
|
||||||
|
val currentDownloadItem = queue[uuid] ?: return@Observer
|
||||||
|
currentDownloadItem.progress = update.getInt("progress", 0)
|
||||||
|
currentDownloadItem.failed = update.getInt("failed", 0)
|
||||||
|
currentDownloadItem.downloaded = update.getInt("downloaded", 0)
|
||||||
|
currentDownloadItem.status = update.getString("status").toString()
|
||||||
|
if (currentDownloadItem.status != "inQueue" && currentDownloadItem.status != "downloading"){
|
||||||
|
currentDownloadItem.progress = 100
|
||||||
|
}
|
||||||
|
val viewHolder = downloadQueueView.findViewHolderForAdapterPosition(position)
|
||||||
|
if (viewHolder != null){
|
||||||
|
(viewHolder as DownloadItemAdapter.DownloadViewHolder).updateDownloadItem(currentDownloadItem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setupPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupPermissions() {
|
||||||
|
val shouldAskPermission: ArrayList<String> = ArrayList()
|
||||||
|
for (permission in permissions){
|
||||||
|
val hasPermission = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
println("$permission, $hasPermission")
|
||||||
|
if (!hasPermission){
|
||||||
|
shouldAskPermission.add(permission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAskPermission.size > 0) {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setMessage("Permission to write and read to storage is required by this app.")
|
||||||
|
.setTitle("Permission required")
|
||||||
|
builder.setPositiveButton("OK") { _, _ ->
|
||||||
|
ActivityCompat.requestPermissions(this, shouldAskPermission.toTypedArray(), 1)
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
} else {
|
||||||
|
attemptAutoLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == 1) {
|
||||||
|
var accepted = true
|
||||||
|
for (grant in grantResults){
|
||||||
|
accepted = accepted && grant == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
if (!accepted) {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setMessage("The app can't work if it can't write to the storage")
|
||||||
|
.setTitle("Permissions not granted")
|
||||||
|
builder.setPositiveButton("OK") { _, _ ->
|
||||||
|
finishAndRemoveTask()
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
} else {
|
||||||
|
attemptAutoLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attemptAutoLogin(){
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
if (dz.isLoggedIn){
|
||||||
|
init()
|
||||||
|
} else {
|
||||||
|
// Try retrieving the login data
|
||||||
|
var loginText: String
|
||||||
|
try {
|
||||||
|
openFileInput("login").use { stream ->
|
||||||
|
loginText = stream.bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception){ loginText = "" }
|
||||||
|
|
||||||
|
// No login file, show login fragment
|
||||||
|
if (loginText == ""){
|
||||||
|
showLoginActivity()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the login text file
|
||||||
|
val splitLogin = loginText.split("\n")
|
||||||
|
val accessToken: String = splitLogin[0]
|
||||||
|
var arl: String = splitLogin[1]
|
||||||
|
|
||||||
|
// Try to login
|
||||||
|
var loggedIn = dz.login(arl)
|
||||||
|
if (loggedIn) {
|
||||||
|
init(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get a new arl from accessToken
|
||||||
|
if (accessToken != ""){
|
||||||
|
val testArl = dz.getArlFromAccessToken(accessToken)
|
||||||
|
if (testArl == null){
|
||||||
|
showLoginActivity()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
arl = testArl
|
||||||
|
loggedIn = dz.login(arl)
|
||||||
|
if (loggedIn) {
|
||||||
|
saveLogin(accessToken, arl) // if the arl changed, save it
|
||||||
|
init(true)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login failed, remove the file and show the login form
|
||||||
|
forgetLogin()
|
||||||
|
showLoginActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoginActivity(){
|
||||||
|
val thisIntent = Intent(this, LoginActivity::class.java)
|
||||||
|
thisIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain"){
|
||||||
|
thisIntent.action = Intent.ACTION_SEND
|
||||||
|
thisIntent.type = "text/plain"
|
||||||
|
thisIntent.putExtra(Intent.EXTRA_TEXT, intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||||
|
}
|
||||||
|
startActivity(thisIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLogin(accessToken: String, arl: String) {
|
||||||
|
openFileOutput("login", Context.MODE_PRIVATE).use { output ->
|
||||||
|
output.write("$accessToken\n$arl".toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun forgetLogin(){
|
||||||
|
deleteFile("login")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(justLoggedIn: Boolean = false){
|
||||||
|
if (!hasBeenInit){
|
||||||
|
// Restore the queue
|
||||||
|
if (justLoggedIn) {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this, "Logged in as ${dz.currentUser!!.name}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restoreQueue()
|
||||||
|
initSettings()
|
||||||
|
|
||||||
|
// Add deezer links to queue on intent action send text
|
||||||
|
if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain"){
|
||||||
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
val url = "https://${text?.substringAfterLast("https://")}"
|
||||||
|
if (URLUtil.isValidUrl(url) and url.contains("deezer")){
|
||||||
|
addUrlToQueue(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasBeenInit = true
|
||||||
|
}
|
||||||
|
runOnUiThread { findViewById<ConstraintLayout>(R.id.loadingScreen).visibility = View.GONE }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initSettings() {
|
||||||
|
if (sharedPreferences.getString("download_quality", "0") == "0"){
|
||||||
|
var quality = 1
|
||||||
|
if (dz.currentUser!!.canStream.high) quality = 3
|
||||||
|
if (dz.currentUser!!.canStream.lossless) quality = 9
|
||||||
|
sharedPreferences.edit()
|
||||||
|
.putString("download_quality", quality.toString())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
override fun onResume(){
|
||||||
|
super.onResume()
|
||||||
|
adapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUrlToQueue(url: String){
|
||||||
|
// Extend and clean the URL if needed
|
||||||
|
var link = url
|
||||||
|
if (link.contains("deezer.page.link")) link = getFinalURL(link)
|
||||||
|
if (link.contains("?")) link = link.substringBefore('?')
|
||||||
|
if (link.contains("&")) link = link.substringBefore('&')
|
||||||
|
if (link.endsWith("/")) link = link.substring(0, link.length-1)
|
||||||
|
|
||||||
|
// Find the download type
|
||||||
|
var downloadType: String? = null
|
||||||
|
when {
|
||||||
|
"/track/" in link -> downloadType = "track"
|
||||||
|
"/album/" in link -> downloadType = "album"
|
||||||
|
//"/artist/" in link -> downloadType = "artist"
|
||||||
|
//"/playlist/" in link -> downloadType = "playlist"
|
||||||
|
}
|
||||||
|
if (downloadType == null){
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Type not recognized!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the download ID
|
||||||
|
val downloadId: String? = """/$downloadType/(.+)""".toRegex().find(link)?.groups?.get(1)?.value
|
||||||
|
if (downloadId == null) {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Couldn't find the id!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the download item
|
||||||
|
val downloadItem: DownloadItem
|
||||||
|
try {
|
||||||
|
downloadItem = when (downloadType) {
|
||||||
|
"track" -> generateTrackItem(downloadId)
|
||||||
|
"album" -> generateAlbumItem(downloadId)
|
||||||
|
//"artist" -> downloadItem = generateArtistItem(downloadId)
|
||||||
|
//"playlist" -> downloadItem = generatePlaylistItem(downloadId)
|
||||||
|
else -> throw Exception("Type not reconized")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Error while trying to generate the download item!\n$e",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
Log.e("deemix", e.message, e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
downloadItem.bitrate = sharedPreferences.getString("download_quality", "1")!!
|
||||||
|
downloadItem.generateUUID()
|
||||||
|
|
||||||
|
// Check if item is already in queue
|
||||||
|
if (queue.keys.contains(downloadItem.uuid)){
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Already in queue!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add item to viewable queue
|
||||||
|
downloadItem.status = "inQueue"
|
||||||
|
fullQueue.add(downloadItem.uuid)
|
||||||
|
queue[downloadItem.uuid] = downloadItem.minimalClone()
|
||||||
|
runOnUiThread {
|
||||||
|
adapter.notifyItemChanged(fullQueue.indexOf(downloadItem.uuid))
|
||||||
|
}
|
||||||
|
|
||||||
|
openFileOutput("queue", Context.MODE_PRIVATE).use { output ->
|
||||||
|
output.write(TextUtils.join(";", fullQueue).toByteArray())
|
||||||
|
}
|
||||||
|
openFileOutput("${downloadItem.uuid}.json", Context.MODE_PRIVATE).use { output ->
|
||||||
|
output.write(downloadItem.toString().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to actual queue
|
||||||
|
addToQueue(downloadItem.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAlreadyInQueue(uuid: String): Boolean{
|
||||||
|
val jobs = WorkManager.getInstance(this).getWorkInfosByTag(uuid).get()
|
||||||
|
return jobs.size > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addToQueue(uuid: String) {
|
||||||
|
val inputData: Data = Data.Builder()
|
||||||
|
.putString("uuid", uuid)
|
||||||
|
.build()
|
||||||
|
val downloadWork: OneTimeWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||||
|
.setInputData(inputData)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.addTag(uuid)
|
||||||
|
.build()
|
||||||
|
WorkManager
|
||||||
|
.getInstance(this)
|
||||||
|
.enqueueUniqueWork(
|
||||||
|
"downloadQueue",
|
||||||
|
ExistingWorkPolicy.APPEND,
|
||||||
|
downloadWork
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeFromQueue(uuid: String, position: Int){
|
||||||
|
fullQueue.removeAt(position)
|
||||||
|
runOnUiThread { adapter.notifyItemRemoved(position) }
|
||||||
|
openFileOutput("queue", Context.MODE_PRIVATE).use { output ->
|
||||||
|
output.write(TextUtils.join(";", fullQueue).toByteArray())
|
||||||
|
}
|
||||||
|
val deletedItem = queue[uuid] ?: return
|
||||||
|
if (deletedItem.status == "downloading" || deletedItem.status == "inQueue"){
|
||||||
|
WorkManager.getInstance(this).cancelAllWorkByTag(uuid)
|
||||||
|
runOnUiThread { Toast.makeText(
|
||||||
|
this@MainActivity,
|
||||||
|
"Removed $uuid from queue",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show() }
|
||||||
|
}
|
||||||
|
deleteFile("$uuid.json")
|
||||||
|
queue.remove(uuid)
|
||||||
|
WorkManager.getInstance(this).pruneWork()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreQueue() {
|
||||||
|
fullQueue.clear()
|
||||||
|
var queueText: String
|
||||||
|
try {
|
||||||
|
openFileInput("queue").use { stream ->
|
||||||
|
queueText = stream.bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception){
|
||||||
|
queueText = ""
|
||||||
|
}
|
||||||
|
val thisQueue = queueText.split(';')
|
||||||
|
for (uuid in thisQueue){
|
||||||
|
if (uuid.isBlank()) continue
|
||||||
|
fullQueue.add(uuid)
|
||||||
|
var downloadObject: JSONObject
|
||||||
|
|
||||||
|
try {
|
||||||
|
openFileInput("$uuid.json").use { stream ->
|
||||||
|
val text = stream.bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
downloadObject = JSONObject(text)
|
||||||
|
}
|
||||||
|
} catch (e: Exception){
|
||||||
|
fullQueue.remove(uuid)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadItem: DownloadItem? = null
|
||||||
|
if (downloadObject.getString("itemType") == "single") downloadItem = Single(downloadObject)
|
||||||
|
else if (downloadObject.getString("itemType") == "collection") downloadItem = Collection(downloadObject)
|
||||||
|
if (downloadItem != null){
|
||||||
|
queue[uuid] = downloadItem.minimalClone()
|
||||||
|
if (downloadItem.status == "inQueue" && !isAlreadyInQueue(downloadItem.uuid)){
|
||||||
|
addToQueue(downloadItem.uuid)
|
||||||
|
}
|
||||||
|
runOnUiThread {adapter.notifyItemChanged(fullQueue.indexOf(uuid))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateTrackItem(downloadId: String, track: Track? = null, album: Album? = null): DownloadItem {
|
||||||
|
val item = Single()
|
||||||
|
val thisTrack: Track
|
||||||
|
|
||||||
|
if (track == null) {
|
||||||
|
thisTrack = Track()
|
||||||
|
if (downloadId.toInt() < 0){
|
||||||
|
val trackAPI = dz.apiCallGW("song.getData", "{\"SNG_ID\": $downloadId}")
|
||||||
|
thisTrack.parseGW(trackAPI)
|
||||||
|
} else {
|
||||||
|
val trackAPI = dz.apiCall("track/$downloadId")
|
||||||
|
thisTrack.parseAPI(trackAPI)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thisTrack = track
|
||||||
|
}
|
||||||
|
|
||||||
|
item.id = thisTrack.id
|
||||||
|
item.type = "track"
|
||||||
|
item.title = thisTrack.title!!
|
||||||
|
item.artist = thisTrack.artist.name
|
||||||
|
item.explicit = thisTrack.explicit == true
|
||||||
|
item.cover = thisTrack.album.pic.getURL(72, "jpg-80")
|
||||||
|
|
||||||
|
val single = JSONObject()
|
||||||
|
single.put("trackAPI", thisTrack.toJSON())
|
||||||
|
if (album != null) single.put("albumAPI", album.toJSON())
|
||||||
|
item.single = single
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateAlbumItem(downloadId: String, rootArtist: Artist? = null): DownloadItem {
|
||||||
|
val item = Collection()
|
||||||
|
val thisAlbum = Album()
|
||||||
|
var thisId = downloadId
|
||||||
|
var albumAPI: JSONObject? = null
|
||||||
|
|
||||||
|
if (thisId.startsWith("upc")){
|
||||||
|
val upcs = ArrayList<String>()
|
||||||
|
upcs.add(thisId.substring(4))
|
||||||
|
upcs.add(upcs[0].toInt().toString()) // Try UPC without leading zeros as well
|
||||||
|
var lastError = Exception()
|
||||||
|
for (upc in upcs){
|
||||||
|
try {
|
||||||
|
albumAPI = dz.apiCall("album/upc:$upc")
|
||||||
|
break
|
||||||
|
} catch (e: Exception){
|
||||||
|
lastError = e
|
||||||
|
albumAPI = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (albumAPI == null){
|
||||||
|
throw Exception("Generation Error: $thisId, $lastError")
|
||||||
|
}
|
||||||
|
thisId = albumAPI.getString("id")
|
||||||
|
} else {
|
||||||
|
albumAPI = dz.apiCall("album/$thisId")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get extra info about album
|
||||||
|
// This saves extra api calls when downloading
|
||||||
|
val albumGW: JSONObject = dz.apiCallGW("album.getData", "{\"ALB_ID\": $thisId}")
|
||||||
|
|
||||||
|
thisAlbum.parseAPI(albumAPI)
|
||||||
|
thisAlbum.parseGW(albumGW)
|
||||||
|
thisAlbum.rootArtist = rootArtist
|
||||||
|
|
||||||
|
var songs = JSONArray()
|
||||||
|
var songsFromGW = false
|
||||||
|
val data = albumAPI.getJSONObject("tracks")
|
||||||
|
if (data.length() == thisAlbum.trackTotal!!.toInt()){
|
||||||
|
songs = data.getJSONArray("data")
|
||||||
|
}
|
||||||
|
if (songs.length() == 0){
|
||||||
|
val body = dz.apiCallGW("song.getListByAlbum", "{\"ALB_ID\": $thisId, \"nb\": -1}")
|
||||||
|
songs = body.getJSONArray("data")
|
||||||
|
songsFromGW = true
|
||||||
|
}
|
||||||
|
|
||||||
|
thisAlbum.trackTotal = songs.length().toString()
|
||||||
|
val tracks = JSONArray()
|
||||||
|
for (i in 0 until songs.length()) {
|
||||||
|
val currentTrackAPI = songs.getJSONObject(i)
|
||||||
|
val track = Track()
|
||||||
|
if (songsFromGW) track.parseGW(currentTrackAPI)
|
||||||
|
else track.parseAPI(currentTrackAPI)
|
||||||
|
track.position = i+1
|
||||||
|
tracks.put(track.toJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracks.length() == 1){
|
||||||
|
val singleTrack = Track().restoreObject(tracks.getJSONObject(0))
|
||||||
|
return generateTrackItem(singleTrack.id, singleTrack, thisAlbum)
|
||||||
|
}
|
||||||
|
|
||||||
|
item.id = thisAlbum.id
|
||||||
|
item.type = "album"
|
||||||
|
item.title = thisAlbum.title
|
||||||
|
item.artist = thisAlbum.artist.name
|
||||||
|
item.explicit = thisAlbum.explicit == true
|
||||||
|
item.size = songs.length()
|
||||||
|
item.cover = thisAlbum.pic.getURL(72, "jpg-80")
|
||||||
|
|
||||||
|
val collection = JSONObject()
|
||||||
|
collection.put("tracks", tracks)
|
||||||
|
collection.put("albumAPI", thisAlbum.toJSON())
|
||||||
|
item.collection = collection
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
private fun generateArtistItem(downloadId: String): DownloadItem {
|
||||||
|
|
||||||
|
val item = DownloadItem()
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePlaylistItem(downloadId: String): DownloadItem {
|
||||||
|
val item = DownloadItem()
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
77
app/src/main/java/app/deemix/downloader/SettingsActivity.kt
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package app.deemix.downloader
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import app.deemix.downloader.SharedObjects.dz
|
||||||
|
import app.deemix.downloader.types.DeezerQuality
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
private lateinit var loggedInPanel:ConstraintLayout
|
||||||
|
private lateinit var userAvatar:ImageView
|
||||||
|
private lateinit var userName:TextView
|
||||||
|
private lateinit var userInfo:TextView
|
||||||
|
private lateinit var logoutButton:Button
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.settings_activity)
|
||||||
|
supportFragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.preferencesFragment, SettingsFragment())
|
||||||
|
.commit()
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
loggedInPanel = findViewById<ConstraintLayout>(R.id.loggedInSection)
|
||||||
|
userAvatar = findViewById<ImageView>(R.id.userAvatar)
|
||||||
|
userName = findViewById<TextView>(R.id.userName)
|
||||||
|
userInfo = findViewById<TextView>(R.id.userInfo)
|
||||||
|
logoutButton = findViewById<Button>(R.id.logoutButton)
|
||||||
|
|
||||||
|
logoutButton.setOnClickListener {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
dz.logout()
|
||||||
|
deleteFile("login")
|
||||||
|
val intent = Intent(this@SettingsActivity, LoginActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
this@SettingsActivity.startActivity(intent)
|
||||||
|
this@SettingsActivity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLoginPanels()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDisplaySubscription(canStream: DeezerQuality): String{
|
||||||
|
if (canStream.lossless) return "Hi-Fi"
|
||||||
|
if (canStream.high) return "Premium"
|
||||||
|
return "Free"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLoginPanels(){
|
||||||
|
if (dz.isLoggedIn){
|
||||||
|
userName.text = dz.currentUser!!.name
|
||||||
|
userInfo.text = "${getDisplaySubscription(dz.currentUser!!.canStream)} | ${dz.currentUser!!.country}"
|
||||||
|
Picasso.get().load("https://e-cdns-images.dzcdn.net/images/user/${dz.currentUser!!.picture}/125x125-000000-80-0-0.jpg")
|
||||||
|
.into(userAvatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
app/src/main/java/app/deemix/downloader/SharedObjects.kt
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package app.deemix.downloader
|
||||||
|
|
||||||
|
import app.deemix.downloader.types.DownloadItem
|
||||||
|
|
||||||
|
object SharedObjects {
|
||||||
|
var dz = Deezer()
|
||||||
|
|
||||||
|
var fullQueue: ArrayList<String> = ArrayList()
|
||||||
|
var queue: MutableMap<String, DownloadItem> = HashMap()
|
||||||
|
}
|
112
app/src/main/java/app/deemix/downloader/Utils.kt
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package app.deemix.downloader
|
||||||
|
|
||||||
|
import android.webkit.URLUtil
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
object Utils {
|
||||||
|
private const val secret = "g4el58wc0zvf9na1"
|
||||||
|
private val secretIvSpec = IvParameterSpec(byteArrayOf(0,1,2,3,4,5,6,7))
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getFinalURL(url: String): String {
|
||||||
|
val con: HttpURLConnection = URL(url).openConnection() as HttpURLConnection
|
||||||
|
con.instanceFollowRedirects = false
|
||||||
|
con.connect()
|
||||||
|
con.inputStream
|
||||||
|
if (con.responseCode == HttpURLConnection.HTTP_MOVED_PERM || con.responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
|
||||||
|
val redirectUrl: String = con.getHeaderField("Location")
|
||||||
|
if (URLUtil.isValidUrl(redirectUrl)) return getFinalURL(redirectUrl)
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bitwiseXor(firstVal: Char, secondVal: Char, thirdVal: Char): Char {
|
||||||
|
return (BigInteger(byteArrayOf(firstVal.code.toByte())) xor
|
||||||
|
BigInteger(byteArrayOf(secondVal.code.toByte())) xor
|
||||||
|
BigInteger(byteArrayOf(thirdVal.code.toByte()))).toByte().toInt().toChar()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBlowfishKey(trackId: String): String {
|
||||||
|
val trackMd5Hex = trackId.toMD5()
|
||||||
|
var blowfishKey = ""
|
||||||
|
|
||||||
|
for (i in 0..15) {
|
||||||
|
val nextChar = bitwiseXor(trackMd5Hex[i], trackMd5Hex[i + 16], secret[i])
|
||||||
|
blowfishKey += nextChar
|
||||||
|
}
|
||||||
|
|
||||||
|
return blowfishKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptBlowfish(chunk: ByteArray, blowfishKey: String): ByteArray {
|
||||||
|
val secretKeySpec = SecretKeySpec(blowfishKey.toByteArray(), "Blowfish")
|
||||||
|
val thisTrackCipher = Cipher.getInstance("BLOWFISH/CBC/NoPadding")
|
||||||
|
thisTrackCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, secretIvSpec)
|
||||||
|
return thisTrackCipher.update(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun concatTitleVersion(titleShort: String, titleVersion: String): String {
|
||||||
|
var thisTitle = titleShort.trim()
|
||||||
|
val thisVersion = titleVersion.trim()
|
||||||
|
if (!thisTitle.contains(thisVersion)) thisTitle += " $thisVersion"
|
||||||
|
return thisTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isExplicit(explicitLyrics: Int): Boolean{
|
||||||
|
return arrayOf(1, 4).contains(explicitLyrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bytesToHex(bytes: ByteArray): String {
|
||||||
|
var hexString = ""
|
||||||
|
for (byte in bytes) {
|
||||||
|
hexString += String.format("%02X", byte)
|
||||||
|
}
|
||||||
|
return hexString
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toMD5(): String {
|
||||||
|
val bytes = MessageDigest.getInstance("MD5").digest(this.toByteArray(Charsets.ISO_8859_1))
|
||||||
|
return bytesToHex(bytes).lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> ArrayList<T>.toJSONArray(): JSONArray {
|
||||||
|
val result = JSONArray()
|
||||||
|
for (value in this){
|
||||||
|
result.put(value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> JSONArray.toArrayList(): ArrayList<T>{
|
||||||
|
val result = ArrayList<T>()
|
||||||
|
for (i in 0 until this.length()) {
|
||||||
|
result.add(this.get(i) as T)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> MutableMap<String, ArrayList<T>>.toJSON(): JSONObject{
|
||||||
|
val result = JSONObject()
|
||||||
|
for (key in this.keys){
|
||||||
|
result.put(key, this[key]!!.toJSONArray())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> JSONObject.toHashMap(): MutableMap<String, ArrayList<T>>{
|
||||||
|
val result: MutableMap<String, ArrayList<T>> = HashMap()
|
||||||
|
for (key in this.keys()){
|
||||||
|
result[key] = this.getJSONArray(key).toArrayList()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
188
app/src/main/java/app/deemix/downloader/types/Album.kt
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import app.deemix.downloader.Utils.concatTitleVersion
|
||||||
|
import app.deemix.downloader.Utils.isExplicit
|
||||||
|
import app.deemix.downloader.toArrayList
|
||||||
|
import app.deemix.downloader.toHashMap
|
||||||
|
import app.deemix.downloader.toJSON
|
||||||
|
import app.deemix.downloader.toJSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
class Album {
|
||||||
|
var id = "0"
|
||||||
|
var title = ""
|
||||||
|
var pic = DeezerPicture()
|
||||||
|
var artist = Artist()
|
||||||
|
var artistRoles: MutableMap<String, ArrayList<String>> = HashMap()
|
||||||
|
var artists: ArrayList<String> = ArrayList()
|
||||||
|
var date: DeezerDate? = null
|
||||||
|
var trackTotal: String? = null
|
||||||
|
var discTotal: String? = null
|
||||||
|
var embeddedCoverPath: String? = null
|
||||||
|
var embeddedCoverURL: String? = null
|
||||||
|
var explicit: Boolean? = null
|
||||||
|
var genres: ArrayList<String>? = null
|
||||||
|
var barcode: String? = null
|
||||||
|
var label: String? = null
|
||||||
|
var copyright: String? = null
|
||||||
|
var recordType: String = "album"
|
||||||
|
var bitrate: String? = null
|
||||||
|
var rootArtist: Artist? = null
|
||||||
|
var variousArtists: Artist? = null
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
pic.type = "cover"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAPI(albumAPI: JSONObject): Album {
|
||||||
|
id = albumAPI.getString("id")
|
||||||
|
title = albumAPI.getString("title")
|
||||||
|
pic.md5 = albumAPI.getString("md5_image")
|
||||||
|
artist.parseAPI(albumAPI.getJSONObject("artist"))
|
||||||
|
val data = albumAPI.getJSONArray("contributors")
|
||||||
|
for (i in 0 until data.length()) {
|
||||||
|
val artist = data.getJSONObject(i)
|
||||||
|
val isVariousArtist = artist.getString("id") == "5080"
|
||||||
|
val isMainArtist = artist.getString("role") == "Main"
|
||||||
|
|
||||||
|
if (isVariousArtist) {
|
||||||
|
variousArtists = Artist().parseAPI(artist)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistName = artist.getString("name")
|
||||||
|
val artistRole = artist.getString("role")
|
||||||
|
|
||||||
|
if (!artists.contains(artistName))
|
||||||
|
artists.add(artistName)
|
||||||
|
|
||||||
|
if (isMainArtist || !artistRoles["Main"]!!.contains(artistName) && !isMainArtist){
|
||||||
|
if (!artistRoles.containsKey(artistRole))
|
||||||
|
artistRoles[artistRole] = ArrayList()
|
||||||
|
artistRoles[artistRole]!!.add(artistName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date = DeezerDate(albumAPI.getString("release_date"))
|
||||||
|
trackTotal = albumAPI.getString("nb_tracks")
|
||||||
|
explicit = albumAPI.getBoolean("explicit_lyrics")
|
||||||
|
val thisGenres = albumAPI.getJSONObject("genres").getJSONArray("data")
|
||||||
|
genres = ArrayList()
|
||||||
|
for (i in 0 until thisGenres.length()) {
|
||||||
|
val genre = thisGenres.getJSONObject(i).getString("name")
|
||||||
|
if (!genres!!.contains(genre)) genres!!.add(genre)
|
||||||
|
}
|
||||||
|
barcode = albumAPI.getString("upc")
|
||||||
|
label = albumAPI.getString("label")
|
||||||
|
recordType = albumAPI.getString("record_type")
|
||||||
|
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseGW(albumAPI: JSONObject): Album {
|
||||||
|
id = albumAPI.getString("ALB_ID")
|
||||||
|
title = albumAPI.getString("ALB_TITLE").trim()
|
||||||
|
pic.md5 = albumAPI.getString("ALB_PICTURE")
|
||||||
|
artist.id = albumAPI.getString("ART_ID")
|
||||||
|
artist.name = albumAPI.getString("ART_NAME")
|
||||||
|
date = DeezerDate(albumAPI.getString("PHYSICAL_RELEASE_DATE"))
|
||||||
|
trackTotal = albumAPI.getString("NUMBER_TRACK")
|
||||||
|
discTotal = albumAPI.getString("NUMBER_DISK")
|
||||||
|
explicit = isExplicit(albumAPI.getJSONObject("EXPLICIT_ALBUM_CONTENT").getInt("EXPLICIT_LYRICS_STATUS"))
|
||||||
|
label = albumAPI.getString("LABEL_NAME")
|
||||||
|
copyright = albumAPI.getString("COPYRIGHT")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseGWPage(albumAPI: JSONObject): Album{
|
||||||
|
id = albumAPI.getString("ALB_ID")
|
||||||
|
title = albumAPI.getString("ALB_TITLE").trim()
|
||||||
|
val version = albumAPI.getString("VERSION").trim()
|
||||||
|
title = concatTitleVersion(title, version)
|
||||||
|
pic.md5 = albumAPI.getString("ALB_PICTURE")
|
||||||
|
artist.id = albumAPI.getString("ART_ID")
|
||||||
|
artist.name = albumAPI.getString("ART_NAME")
|
||||||
|
val data = albumAPI.getJSONArray("ARTISTS")
|
||||||
|
for (i in 0 until data.length()) {
|
||||||
|
val artist = data.getJSONObject(i)
|
||||||
|
val isVariousArtist = artist.getString("ART_ID") == "5080"
|
||||||
|
val isMainArtist = artist.getString("ROLE") == "0"
|
||||||
|
|
||||||
|
if (isVariousArtist) {
|
||||||
|
variousArtists = Artist().parseGW(artist)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistName = artist.getString("ART_NAME")
|
||||||
|
val artistRole = when (artist.getString("ROLE_ID")){
|
||||||
|
"0" -> "Main"
|
||||||
|
"5" -> "Featured"
|
||||||
|
else -> "Other"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artists.contains(artistName))
|
||||||
|
artists.add(artistName)
|
||||||
|
|
||||||
|
if (isMainArtist || !artistRoles["Main"]!!.contains(artistName) && !isMainArtist){
|
||||||
|
if (!artistRoles.containsKey(artistRole))
|
||||||
|
artistRoles[artistRole] = ArrayList()
|
||||||
|
artistRoles[artistRole]!!.add(artistName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date = DeezerDate(albumAPI.getString("PHYSICAL_RELEASE_DATE"))
|
||||||
|
explicit = isExplicit(albumAPI.getJSONObject("EXPLICIT_ALBUM_CONTENT").getInt("EXPLICIT_LYRICS_STATUS"))
|
||||||
|
barcode = albumAPI.getString("UPC")
|
||||||
|
label = albumAPI.getString("LABEL_NAME")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreObject(albumAPI: JSONObject): Album{
|
||||||
|
id = albumAPI.getString("id")
|
||||||
|
title = albumAPI.getString("title")
|
||||||
|
pic.restoreObject(albumAPI.getJSONObject("pic"))
|
||||||
|
artist.restoreObject(albumAPI.getJSONObject("artist"))
|
||||||
|
artistRoles = albumAPI.getJSONObject("artistRoles").toHashMap()
|
||||||
|
artists = albumAPI.getJSONArray("artists").toArrayList()
|
||||||
|
if (albumAPI.has("date")) date = DeezerDate(albumAPI.getString("date"))
|
||||||
|
if (albumAPI.has("trackTotal")) trackTotal = albumAPI.getString("trackTotal")
|
||||||
|
if (albumAPI.has("discTotal")) discTotal = albumAPI.getString("discTotal")
|
||||||
|
if (albumAPI.has("embeddedCoverPath")) embeddedCoverPath = albumAPI.getString("embeddedCoverPath")
|
||||||
|
if (albumAPI.has("embeddedCoverURL")) embeddedCoverURL = albumAPI.getString("embeddedCoverURL")
|
||||||
|
if (albumAPI.has("explicit")) explicit = albumAPI.getBoolean("explicit")
|
||||||
|
if (albumAPI.has("genres")) genres = albumAPI.getJSONArray("genres").toArrayList()
|
||||||
|
if (albumAPI.has("barcode")) barcode = albumAPI.getString("barcode")
|
||||||
|
if (albumAPI.has("label")) label = albumAPI.getString("label")
|
||||||
|
if (albumAPI.has("copyright")) copyright = albumAPI.getString("copyright")
|
||||||
|
if (albumAPI.has("bitrate")) bitrate = albumAPI.getString("bitrate")
|
||||||
|
if (albumAPI.has("rootArtist")) rootArtist = Artist().restoreObject(albumAPI.getJSONObject("rootArtist"))
|
||||||
|
recordType = albumAPI.getString("recordType")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): JSONObject {
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("id", id)
|
||||||
|
obj.put("title", title)
|
||||||
|
obj.put("pic", pic.toJSON())
|
||||||
|
obj.put("artist", artist.toJSON())
|
||||||
|
obj.put("artistRoles", artistRoles.toJSON())
|
||||||
|
obj.put("artists", artists.toJSONArray())
|
||||||
|
if (date != null) obj.put("date", date)
|
||||||
|
if (trackTotal != null) obj.put("trackTotal", trackTotal)
|
||||||
|
if (discTotal != null) obj.put("discTotal", discTotal)
|
||||||
|
if (embeddedCoverPath != null) obj.put("embeddedCoverPath", embeddedCoverPath)
|
||||||
|
if (embeddedCoverURL != null) obj.put("embeddedCoverURL", embeddedCoverURL)
|
||||||
|
if (explicit != null) obj.put("explicit", explicit)
|
||||||
|
if (genres != null) obj.put("genres", genres!!.toJSONArray())
|
||||||
|
if (barcode != null) obj.put("barcode", barcode)
|
||||||
|
if (label != null) obj.put("label", label)
|
||||||
|
if (copyright != null) obj.put("copyright", copyright)
|
||||||
|
if (bitrate != null) obj.put("bitrate", bitrate)
|
||||||
|
if (rootArtist != null) obj.put("rootArtist", rootArtist)
|
||||||
|
obj.put("recordType", recordType)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
55
app/src/main/java/app/deemix/downloader/types/Artist.kt
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Artist {
|
||||||
|
var id: String = "0"
|
||||||
|
var name: String = ""
|
||||||
|
var pic = DeezerPicture()
|
||||||
|
var role: String = "Main"
|
||||||
|
var shouldSave = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
pic.type = "artist"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAPI(artistAPI: JSONObject): Artist{
|
||||||
|
id = artistAPI.getString("id")
|
||||||
|
name = artistAPI.getString("name")
|
||||||
|
if (artistAPI.has("md5_image"))
|
||||||
|
pic.md5 =artistAPI.getString("md5_image")
|
||||||
|
else if (artistAPI.has("picture_small"))
|
||||||
|
pic.md5 = artistAPI.getString("picture_small").substringAfter("/artist/").substringBefore("/")
|
||||||
|
if (artistAPI.has("role")) role = artistAPI.getString("role")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseGW(artistAPI: JSONObject): Artist{
|
||||||
|
id = artistAPI.getString("ART_ID")
|
||||||
|
name = artistAPI.getString("ART_NAME")
|
||||||
|
pic.md5 = artistAPI.getString("ART_PICTURE")
|
||||||
|
role = when (artistAPI.getString("ROLE_ID")){
|
||||||
|
"0" -> "Main"
|
||||||
|
"5" -> "Featured"
|
||||||
|
else -> "Other"
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreObject(artistAPI: JSONObject): Artist {
|
||||||
|
id = artistAPI.getString("id")
|
||||||
|
name = artistAPI.getString("name")
|
||||||
|
pic.restoreObject(artistAPI.getJSONObject("pic"))
|
||||||
|
role = artistAPI.getString("role")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): JSONObject{
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("id", id)
|
||||||
|
obj.put("name", name)
|
||||||
|
obj.put("pic", pic.toJSON())
|
||||||
|
obj.put("role", role)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/java/app/deemix/downloader/types/DeezerDate.kt
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
class DeezerDate {
|
||||||
|
var day: String = "00"
|
||||||
|
var month: String = "00"
|
||||||
|
var year: String = "0000"
|
||||||
|
|
||||||
|
constructor(date: String){
|
||||||
|
year = date.substring(0, 4)
|
||||||
|
month = date.substring(5, 7)
|
||||||
|
day = date.substring(8, 10)
|
||||||
|
fixDayMonth()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fixDayMonth(){
|
||||||
|
if (month.toInt() > 12){
|
||||||
|
val monthTemp = month
|
||||||
|
month = day
|
||||||
|
day = monthTemp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "$year-$month-$day"
|
||||||
|
fun toID3String(): String = "$day$month"
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class DeezerPicture {
|
||||||
|
var md5: String = ""
|
||||||
|
var type: String = ""
|
||||||
|
|
||||||
|
fun getURL(size: Int, format: String): String{
|
||||||
|
val url = "https://e-cdns-images.dzcdn.net/images/$type/$md5/${size}x${size}"
|
||||||
|
|
||||||
|
if (format.startsWith("jpg")){
|
||||||
|
var quality = 80
|
||||||
|
if (format.contains("-")) quality = format.substring(4).toInt()
|
||||||
|
return "$url-000000-$quality-0-0.jpg"
|
||||||
|
}
|
||||||
|
if (format == "png"){
|
||||||
|
return "$url-none-100-0-0.png"
|
||||||
|
}
|
||||||
|
return "$url.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreObject(picture: JSONObject): DeezerPicture{
|
||||||
|
md5 = picture.getString("md5")
|
||||||
|
type = picture.getString("type")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): JSONObject{
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("md5", md5)
|
||||||
|
obj.put("type", type)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
56
app/src/main/java/app/deemix/downloader/types/DeezerUser.kt
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class DeezerQuality(soundQuality: JSONObject) {
|
||||||
|
var low: Boolean = false
|
||||||
|
var standard: Boolean = false
|
||||||
|
var high: Boolean = false
|
||||||
|
var lossless: Boolean = false
|
||||||
|
var reality: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
low = soundQuality.getBoolean("low")
|
||||||
|
standard = soundQuality.getBoolean("standard")
|
||||||
|
high = soundQuality.getBoolean("high")
|
||||||
|
lossless = soundQuality.getBoolean("lossless")
|
||||||
|
reality = soundQuality.getBoolean("reality")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeezerUser(userData: JSONObject, child: JSONObject? = null) {
|
||||||
|
var id: String
|
||||||
|
var name: String
|
||||||
|
var picture: String = ""
|
||||||
|
var licenseToken: String
|
||||||
|
var canStream: DeezerQuality
|
||||||
|
var country: String
|
||||||
|
var language: String
|
||||||
|
var lovedTracksID: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (child != null){
|
||||||
|
id = child.getString("USER_ID")
|
||||||
|
name = child.getString("BLOG_NAME")
|
||||||
|
picture = child.getString("USER_PICTURE")
|
||||||
|
lovedTracksID = child.getString("LOVEDTRACKS_ID")
|
||||||
|
} else {
|
||||||
|
id = userData.getString("USER_ID")
|
||||||
|
name = userData.getString("BLOG_NAME")
|
||||||
|
picture = userData.getString("USER_PICTURE")
|
||||||
|
lovedTracksID = userData.getString("LOVEDTRACKS_ID")
|
||||||
|
}
|
||||||
|
licenseToken = userData.getJSONObject("OPTIONS").getString("license_token")
|
||||||
|
canStream = DeezerQuality(userData.getJSONObject("OPTIONS").getJSONObject("web_sound_quality"))
|
||||||
|
country = userData.getJSONObject("OPTIONS").getString("license_country")
|
||||||
|
var lang = userData.getJSONObject("SETTING").getJSONObject("global").getString("language")
|
||||||
|
.replace("[^0-9A-Za-z *,-.;=]".toRegex(), "")
|
||||||
|
lang = if (lang.length > 2 && lang[2] == '-'){
|
||||||
|
lang.substring(0, 5)
|
||||||
|
} else {
|
||||||
|
lang.substring(0, 2)
|
||||||
|
}
|
||||||
|
language = lang
|
||||||
|
}
|
||||||
|
}
|
141
app/src/main/java/app/deemix/downloader/types/DownloadItem.kt
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open class DownloadItem {
|
||||||
|
var type: String = ""
|
||||||
|
var id: String = ""
|
||||||
|
var bitrate: String = "3"
|
||||||
|
var title: String = ""
|
||||||
|
var artist: String = ""
|
||||||
|
var cover: String = ""
|
||||||
|
var explicit: Boolean = false
|
||||||
|
open var size: Int = 0
|
||||||
|
var downloaded: Int = 0
|
||||||
|
var failed: Int = 0
|
||||||
|
var progress: Int = -1
|
||||||
|
var errors: ArrayList<String> = ArrayList()
|
||||||
|
var uuid: String = ""
|
||||||
|
var isCanceled: Boolean = false
|
||||||
|
open var itemType: String = ""
|
||||||
|
var status: String = ""
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(obj: JSONObject){
|
||||||
|
type = obj.getString("type")
|
||||||
|
id = obj.getString("id")
|
||||||
|
bitrate = obj.getString("bitrate")
|
||||||
|
title = obj.getString("title")
|
||||||
|
artist = obj.getString("artist")
|
||||||
|
cover = obj.getString("cover")
|
||||||
|
explicit = obj.getBoolean("explicit")
|
||||||
|
size = obj.getInt("size")
|
||||||
|
downloaded = obj.getInt("downloaded")
|
||||||
|
failed = obj.getInt("failed")
|
||||||
|
progress = obj.getInt("progress")
|
||||||
|
for (i in 0 until obj.getJSONArray("errors").length()){
|
||||||
|
errors.add(obj.getJSONArray("errors").getString(i))
|
||||||
|
}
|
||||||
|
uuid = obj.getString("uuid")
|
||||||
|
isCanceled = obj.getBoolean("isCanceled")
|
||||||
|
itemType = obj.getString("itemType")
|
||||||
|
status = obj.getString("status")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(obj: DownloadItem){
|
||||||
|
type = obj.type
|
||||||
|
id = obj.id
|
||||||
|
bitrate = obj.bitrate
|
||||||
|
title = obj.title
|
||||||
|
artist = obj.artist
|
||||||
|
cover = obj.cover
|
||||||
|
explicit = obj.explicit
|
||||||
|
size = obj.size
|
||||||
|
downloaded = obj.downloaded
|
||||||
|
failed = obj.failed
|
||||||
|
progress = obj.progress
|
||||||
|
errors = obj.errors
|
||||||
|
uuid = obj.uuid
|
||||||
|
isCanceled = obj.isCanceled
|
||||||
|
itemType = obj.itemType
|
||||||
|
status = obj.status
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateUUID(){
|
||||||
|
uuid = "${type}_${id}_${bitrate}"
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun toJSON(): JSONObject {
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("type", type)
|
||||||
|
obj.put("id", id)
|
||||||
|
obj.put("bitrate", bitrate)
|
||||||
|
obj.put("title", title)
|
||||||
|
obj.put("artist", artist)
|
||||||
|
obj.put("cover", cover)
|
||||||
|
obj.put("explicit", explicit)
|
||||||
|
obj.put("size", size)
|
||||||
|
obj.put("downloaded", downloaded)
|
||||||
|
obj.put("failed", failed)
|
||||||
|
obj.put("progress", progress)
|
||||||
|
obj.put("errors", JSONArray(errors))
|
||||||
|
obj.put("uuid", uuid)
|
||||||
|
obj.put("isCanceled", isCanceled)
|
||||||
|
obj.put("itemType", itemType)
|
||||||
|
obj.put("status", status)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return toJSON().toString(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun minimalClone(): DownloadItem {
|
||||||
|
return DownloadItem(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Single : DownloadItem {
|
||||||
|
override var size = 1
|
||||||
|
override var itemType = "single"
|
||||||
|
var single: JSONObject = JSONObject()
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(obj: JSONObject): super(obj){
|
||||||
|
single = obj.getJSONObject("single")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(obj: Single): super(obj){
|
||||||
|
single = obj.single
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJSON(): JSONObject {
|
||||||
|
val obj = super.toJSON()
|
||||||
|
obj.put("single", single)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Collection : DownloadItem {
|
||||||
|
override var itemType = "collection"
|
||||||
|
var collection: JSONObject = JSONObject()
|
||||||
|
|
||||||
|
constructor()
|
||||||
|
|
||||||
|
constructor(obj: JSONObject): super(obj){
|
||||||
|
collection = obj.getJSONObject("collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(obj: Collection): super(obj){
|
||||||
|
collection = obj.collection
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toJSON(): JSONObject {
|
||||||
|
val obj = super.toJSON()
|
||||||
|
obj.put("collection", collection)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
28
app/src/main/java/app/deemix/downloader/types/Lyrics.kt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import app.deemix.downloader.toArrayList
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Lyrics {
|
||||||
|
var id: String = "0"
|
||||||
|
var sync: String = ""
|
||||||
|
var unsync: String = ""
|
||||||
|
var syncID3: ArrayList< Pair<String, Int>> = ArrayList()
|
||||||
|
|
||||||
|
fun restoreObject(lyricsAPI: JSONObject): Lyrics {
|
||||||
|
id = lyricsAPI.getString("id")
|
||||||
|
sync = lyricsAPI.getString("sync")
|
||||||
|
unsync = lyricsAPI.getString("unsync")
|
||||||
|
//syncID3 = lyricsAPI.getJSONArray("syncID3").toArrayList()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): JSONObject {
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("id", id)
|
||||||
|
obj.put("sync", sync)
|
||||||
|
obj.put("unsync", unsync)
|
||||||
|
//obj.put("syncID3", syncID3) // TODO: toJSON
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
263
app/src/main/java/app/deemix/downloader/types/Track.kt
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
package app.deemix.downloader.types
|
||||||
|
|
||||||
|
import app.deemix.downloader.Utils.concatTitleVersion
|
||||||
|
import app.deemix.downloader.Utils.isExplicit
|
||||||
|
import app.deemix.downloader.toArrayList
|
||||||
|
import app.deemix.downloader.toHashMap
|
||||||
|
import app.deemix.downloader.toJSON
|
||||||
|
import app.deemix.downloader.toJSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class Track {
|
||||||
|
lateinit var id: String
|
||||||
|
|
||||||
|
// Full title
|
||||||
|
var title: String = ""
|
||||||
|
// The title without the version
|
||||||
|
var titleShort: String? = null
|
||||||
|
// Just the version
|
||||||
|
var titleVersion: String? = null
|
||||||
|
|
||||||
|
var trackToken: String? = null
|
||||||
|
var trackTokenExpiration: Int = 0
|
||||||
|
var fallbackID: String? = null
|
||||||
|
|
||||||
|
// Just the main artist of the track
|
||||||
|
var artist: Artist = Artist()
|
||||||
|
// List of all roles with their respective artists
|
||||||
|
var artistRoles: MutableMap<String, ArrayList<String>> = HashMap()
|
||||||
|
// Just all the artists as strings
|
||||||
|
var artists: ArrayList<String> = ArrayList()
|
||||||
|
// All the contributors of the track
|
||||||
|
var contributors: MutableMap<String, ArrayList<String>> = HashMap()
|
||||||
|
|
||||||
|
var album: Album = Album()
|
||||||
|
var date: DeezerDate? = null
|
||||||
|
var lyrics: Lyrics? = null
|
||||||
|
|
||||||
|
var duration: Int = 0 // in milliseconds
|
||||||
|
var trackNumber: String? = null
|
||||||
|
var discNumber: String? = null
|
||||||
|
var bpm: Float? = null
|
||||||
|
var explicit: Boolean? = null
|
||||||
|
var isrc: String? = null
|
||||||
|
var replayGain: Float? = null
|
||||||
|
var isAvailableSomewhere: Boolean? = null
|
||||||
|
var position: Int = 0
|
||||||
|
var searched: Boolean = false
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
var isLocal: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
artistRoles["Main"] = ArrayList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseAPI(trackAPI: JSONObject): Track {
|
||||||
|
id = trackAPI.getString("id")
|
||||||
|
title = trackAPI.getString("title")
|
||||||
|
titleShort = trackAPI.getString("title_short")
|
||||||
|
titleVersion = if (trackAPI.has("titleVersion"))
|
||||||
|
trackAPI.getString("title_version")
|
||||||
|
else ""
|
||||||
|
artist.parseAPI(trackAPI.getJSONObject("artist"))
|
||||||
|
duration = trackAPI.getInt("duration") * 1000
|
||||||
|
explicit = trackAPI.getBoolean("explicit_lyrics")
|
||||||
|
|
||||||
|
if (trackAPI.has("isrc")) isrc = trackAPI.getString("isrc")
|
||||||
|
if (trackAPI.has("track_position")) trackNumber = trackAPI.getString("track_position")
|
||||||
|
if (trackAPI.has("disk_number")) discNumber = trackAPI.getString("disk_number")
|
||||||
|
if (trackAPI.has("bpm")) bpm = trackAPI.getString("bpm").toFloat() // Only on public API
|
||||||
|
if (trackAPI.has("gain")) replayGain = trackAPI.getString("gain").toFloat()
|
||||||
|
if (trackAPI.has("available_countries")) isAvailableSomewhere = trackAPI.getJSONArray("available_countries").length() > 0
|
||||||
|
if (trackAPI.has("contributors")) {
|
||||||
|
val data = trackAPI.getJSONArray("contributors")
|
||||||
|
for (i in 0 until data.length()) {
|
||||||
|
val artist = data.getJSONObject(i)
|
||||||
|
val isVariousArtist = artist.getString("id") == "5080"
|
||||||
|
val isMainArtist = artist.getString("role") == "Main"
|
||||||
|
|
||||||
|
if (data.length() > 1 && isVariousArtist) continue
|
||||||
|
|
||||||
|
val artistName = artist.getString("name")
|
||||||
|
val artistRole = artist.getString("role")
|
||||||
|
|
||||||
|
if (!artists.contains(artistName))
|
||||||
|
artists.add(artistName)
|
||||||
|
|
||||||
|
if (isMainArtist || !artistRoles["Main"]!!.contains(artistName) && !isMainArtist){
|
||||||
|
if (!artistRoles.containsKey(artistRole))
|
||||||
|
artistRoles[artistRole] = ArrayList()
|
||||||
|
artistRoles[artistRole]!!.add(artistName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (trackAPI.has("release_date")) date = DeezerDate(trackAPI.getString("release_date"))
|
||||||
|
if (trackAPI.has("md5_image")) album.pic.md5 = trackAPI.getString("md5_image")
|
||||||
|
if (trackAPI.has("album")) {
|
||||||
|
val thisAlbum = trackAPI.getJSONObject("album")
|
||||||
|
album.id = thisAlbum.getString("id")
|
||||||
|
album.title = thisAlbum.getString("title")
|
||||||
|
album.pic.md5 = thisAlbum.getString("md5_image")
|
||||||
|
album.date = DeezerDate(thisAlbum.getString("release_date"))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseGW(trackAPI: JSONObject): Track{
|
||||||
|
id = trackAPI.getString("SNG_ID")
|
||||||
|
album.id = trackAPI.getString("ALB_ID")
|
||||||
|
album.pic.md5 = trackAPI.getString("ALB_PICTURE")
|
||||||
|
album.title = trackAPI.getString("ALB_TITLE")
|
||||||
|
artist.id = trackAPI.getString("ART_ID")
|
||||||
|
artist.name = trackAPI.getString("ART_NAME")
|
||||||
|
duration = trackAPI.getInt("DURATION") * 1000
|
||||||
|
isrc = trackAPI.getString("ISRC")
|
||||||
|
titleShort = trackAPI.getString("SNG_TITLE")
|
||||||
|
title = titleShort!!
|
||||||
|
trackToken = trackAPI.getString("TRACK_TOKEN")
|
||||||
|
trackTokenExpiration = trackAPI.getInt("TRACK_TOKEN_EXPIRE")
|
||||||
|
isLocal = id.toInt() < 0
|
||||||
|
if (isLocal) return this
|
||||||
|
|
||||||
|
val data = trackAPI.getJSONArray("ARTISTS")
|
||||||
|
for (i in 0 until data.length()) {
|
||||||
|
val artist = data.getJSONObject(i)
|
||||||
|
val isVariousArtist = artist.getString("ART_ID") == "5080"
|
||||||
|
val isMainArtist = artist.getString("ROLE_ID") == "0"
|
||||||
|
|
||||||
|
if (data.length() > 1 && isVariousArtist) continue
|
||||||
|
|
||||||
|
val artistName = artist.getString("ART_NAME")
|
||||||
|
val artistRole = when (artist.getString("ROLE_ID")){
|
||||||
|
"0" -> "Main"
|
||||||
|
"5" -> "Featured"
|
||||||
|
else -> "Other"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artists.contains(artistName))
|
||||||
|
artists.add(artistName)
|
||||||
|
|
||||||
|
if (isMainArtist || !artistRoles["Main"]!!.contains(artistName) && !isMainArtist){
|
||||||
|
if (!artistRoles.containsKey(artistRole))
|
||||||
|
artistRoles[artistRole] = ArrayList()
|
||||||
|
artistRoles[artistRole]!!.add(artistName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discNumber = trackAPI.getString("DISK_NUMBER")
|
||||||
|
explicit = isExplicit(trackAPI.getInt("EXPLICIT_LYRICS"))
|
||||||
|
// GENRE_ID
|
||||||
|
// LYRICS_ID
|
||||||
|
date = DeezerDate(trackAPI.getString("PHYSICAL_RELEASE_DATE"))
|
||||||
|
// contributors = SNG_CONTRIBUTORS
|
||||||
|
trackNumber = trackAPI.getString("TRACK_NUMBER")
|
||||||
|
titleVersion = if (trackAPI.has("VERSION"))
|
||||||
|
trackAPI.getString("VERSION")
|
||||||
|
else ""
|
||||||
|
replayGain = trackAPI.getString("GAIN").toFloat()
|
||||||
|
title = concatTitleVersion(titleShort?:"", titleVersion?:"")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseGWPage(trackAPI: JSONObject): Track{
|
||||||
|
val data = trackAPI.getJSONObject("DATA")
|
||||||
|
parseGW(data)
|
||||||
|
artist.pic.md5 = data.getString("ART_PICTURE")
|
||||||
|
album.copyright = data.getString("COPYRIGHT")
|
||||||
|
// isAvailableSomewhere = AVAILABLE_COUNTRIES
|
||||||
|
|
||||||
|
if (trackAPI.has("LYRICS")){
|
||||||
|
val lyricsData = trackAPI.getJSONObject("LYRICS")
|
||||||
|
}
|
||||||
|
if (trackAPI.has("ISRC")){
|
||||||
|
val isrcData = trackAPI.getJSONObject("ISRC")
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanUp(){
|
||||||
|
// Remove unwanted charaters in track name
|
||||||
|
// Example: track/127793
|
||||||
|
title = title.replace(Regex("\\s\\s+"), " ")
|
||||||
|
|
||||||
|
// Make sure there is at least one artist
|
||||||
|
// and that the first artist is the main one
|
||||||
|
if (artists.contains(artist.name)){
|
||||||
|
if (artists.indexOf(artist.name) != 0){
|
||||||
|
artists.remove(artist.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!artists.contains(artist.name)) artists.add(0, artist.name)
|
||||||
|
if (artistRoles["Main"]!!.contains(artist.name)){
|
||||||
|
if (artistRoles["Main"]!!.indexOf(artist.name) != 0){
|
||||||
|
artistRoles["Main"]!!.remove(artist.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!artistRoles["Main"]!!.contains(artist.name)) artistRoles["Main"]!!.add(0, artist.name)
|
||||||
|
if (album.artists.contains(album.artist.name)){
|
||||||
|
if (album.artists.indexOf(album.artist.name) != 0){
|
||||||
|
album.artists.remove(album.artist.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!album.artists.contains(album.artist.name)) album.artists.add(0, album.artist.name)
|
||||||
|
if (album.artistRoles["Main"]!!.contains(album.artist.name)){
|
||||||
|
if (album.artistRoles["Main"]!!.indexOf(album.artist.name) != 0){
|
||||||
|
album.artistRoles["Main"]!!.remove(album.artist.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!album.artistRoles["Main"]!!.contains(album.artist.name)) album.artistRoles["Main"]!!.add(0, album.artist.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreObject(trackAPI: JSONObject): Track {
|
||||||
|
id = trackAPI.getString("id")
|
||||||
|
if (trackAPI.has("title")) title = trackAPI.getString("title")
|
||||||
|
if (trackAPI.has("titleShort")) titleShort = trackAPI.getString("titleShort")
|
||||||
|
if (trackAPI.has("titleVersion")) titleVersion = trackAPI.getString("titleVersion")
|
||||||
|
if (trackAPI.has("fallbackID")) fallbackID = trackAPI.getString("fallbackID")
|
||||||
|
artist.restoreObject(trackAPI.getJSONObject("artist"))
|
||||||
|
artistRoles = trackAPI.getJSONObject("artistRoles").toHashMap()
|
||||||
|
contributors = trackAPI.getJSONObject("contributors").toHashMap()
|
||||||
|
artists = trackAPI.getJSONArray("artists").toArrayList()
|
||||||
|
album.restoreObject(trackAPI.getJSONObject("album"))
|
||||||
|
if (trackAPI.has("date")) date = DeezerDate(trackAPI.getString("date"))
|
||||||
|
if (trackAPI.has("lyrics")) lyrics = Lyrics().restoreObject(trackAPI.getJSONObject("lyrics"))
|
||||||
|
duration = trackAPI.getInt("duration")
|
||||||
|
if (trackAPI.has("trackNumber")) trackNumber = trackAPI.getString("trackNumber")
|
||||||
|
if (trackAPI.has("discNumber")) discNumber = trackAPI.getString("discNumber")
|
||||||
|
if (trackAPI.has("bpm")) bpm = trackAPI.getString("bpm").toFloat()
|
||||||
|
if (trackAPI.has("explicit")) explicit = trackAPI.getBoolean("explicit")
|
||||||
|
if (trackAPI.has("isrc")) isrc = trackAPI.getString("isrc")
|
||||||
|
if (trackAPI.has("replayGain")) replayGain = trackAPI.getString("replayGain").toFloat()
|
||||||
|
if (trackAPI.has("isAvailableSomewhere")) isAvailableSomewhere = trackAPI.getBoolean("isAvailableSomewhere")
|
||||||
|
position = trackAPI.getInt("position")
|
||||||
|
searched = trackAPI.getBoolean("searched")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJSON(): JSONObject{
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("id", id)
|
||||||
|
obj.put("title", title)
|
||||||
|
if (titleShort != null) obj.put("titleShort", titleShort)
|
||||||
|
if (titleVersion != null) obj.put("titleVersion", titleVersion)
|
||||||
|
if (fallbackID != null) obj.put("fallbackID", fallbackID)
|
||||||
|
obj.put("artist", artist.toJSON())
|
||||||
|
obj.put("artistRoles", artistRoles.toJSON())
|
||||||
|
obj.put("contributors", contributors.toJSON())
|
||||||
|
obj.put("artists", artists.toJSONArray())
|
||||||
|
obj.put("album", album.toJSON())
|
||||||
|
if (date != null) obj.put("date", date.toString())
|
||||||
|
if (lyrics != null) obj.put("lyrics", lyrics!!.toJSON())
|
||||||
|
obj.put("duration", duration)
|
||||||
|
if (trackNumber != null) obj.put("trackNumber", trackNumber)
|
||||||
|
if (discNumber != null) obj.put("discNumber", discNumber)
|
||||||
|
if (bpm != null) obj.put("bpm", bpm)
|
||||||
|
if (explicit != null) obj.put("explicit", explicit)
|
||||||
|
if (isrc != null) obj.put("isrc", isrc)
|
||||||
|
if (replayGain != null) obj.put("replayGain", replayGain)
|
||||||
|
if (isAvailableSomewhere != null) obj.put("isAvailableSomewhere", isAvailableSomewhere)
|
||||||
|
obj.put("position", position)
|
||||||
|
obj.put("searched", searched)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
44
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:pathData="m31.502,31c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.504 0.502,0.504h6.17c0.278,0 0.504,-0.225 0.504,-0.504v-6.17c0,-0.278 -0.225,-0.502 -0.504,-0.502zM31.502,38.765c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.504,-0.224 0.504,-0.502v-6.17c0,-0.278 -0.225,-0.502 -0.504,-0.502zM39.268,38.765c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,38.765c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM31.502,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.504,-0.224 0.504,-0.502v-6.172c0,-0.278 -0.225,-0.502 -0.504,-0.502zM39.268,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM70.327,46.529c-0.278,0 -0.504,0.224 -0.504,0.502v6.172c0,0.278 0.225,0.502 0.504,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM31.502,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.504,-0.224 0.504,-0.502v-6.172c0,-0.278 -0.225,-0.502 -0.504,-0.502zM39.268,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM47.032,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM70.327,54.294c-0.278,0 -0.504,0.224 -0.504,0.502v6.172c0,0.278 0.225,0.502 0.504,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM31.502,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.504,-0.224 0.504,-0.502v-6.17c0,-0.278 -0.225,-0.502 -0.504,-0.502zM39.268,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM47.032,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM70.327,62.06c-0.278,0 -0.504,0.224 -0.504,0.502v6.17c0,0.278 0.225,0.502 0.504,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM31.502,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.504,-0.224 0.504,-0.502v-6.17c0,-0.278 -0.225,-0.504 -0.504,-0.504zM39.268,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504zM47.032,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504zM54.797,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504zM62.562,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504zM70.327,69.824c-0.278,0 -0.504,0.225 -0.504,0.504v6.17c0,0.278 0.225,0.502 0.504,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="3.0664"
|
||||||
|
android:strokeLineCap="round">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="30.999683"
|
||||||
|
android:startX="53.80671"
|
||||||
|
android:endY="77.000305"
|
||||||
|
android:endX="53.80671"
|
||||||
|
android:type="linear"
|
||||||
|
android:tileMode="clamp">
|
||||||
|
<item android:offset="0" android:color="#FF007AFF"/>
|
||||||
|
<item android:offset="1" android:color="#FF5856D6"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="m62.562,38.765c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,46.529c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,54.294c-0.278,0 -0.502,0.224 -0.502,0.502v6.172c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.172c0,-0.278 -0.224,-0.502 -0.502,-0.502zM47.032,62.06c-0.088,0 -0.166,0.028 -0.238,0.068l6.844,6.844c0.04,-0.072 0.068,-0.151 0.068,-0.239v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM54.797,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.172c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM62.562,62.06c-0.278,0 -0.502,0.224 -0.502,0.502v6.17c0,0.278 0.224,0.502 0.502,0.502h6.17c0.278,0 0.502,-0.224 0.502,-0.502v-6.17c0,-0.278 -0.224,-0.502 -0.502,-0.502zM70.327,62.06c-0.278,0 -0.504,0.224 -0.504,0.502v6.17c0,0.033 0.013,0.061 0.019,0.092l6.746,-6.746c-0.03,-0.006 -0.058,-0.019 -0.09,-0.019zM54.797,69.824c-0.088,0 -0.167,0.028 -0.239,0.068l6.846,6.846c0.04,-0.072 0.068,-0.151 0.068,-0.239v-6.17c0,-0.278 -0.224,-0.504 -0.502,-0.504zM62.562,69.824c-0.278,0 -0.502,0.225 -0.502,0.504v6.17c0,0.032 0.013,0.06 0.019,0.09l6.746,-6.746c-0.031,-0.006 -0.059,-0.019 -0.092,-0.019z"
|
||||||
|
android:strokeLineJoin="miter"
|
||||||
|
android:strokeWidth="0.920009"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeLineCap="butt">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="38.66649"
|
||||||
|
android:startX="61.66677"
|
||||||
|
android:endY="57.414715"
|
||||||
|
android:endX="61.661545"
|
||||||
|
android:type="linear"
|
||||||
|
android:tileMode="clamp">
|
||||||
|
<item android:offset="0" android:color="#FF007AFF"/>
|
||||||
|
<item android:offset="1" android:color="#FFFFFFFF"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/divider.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="1dp"
|
||||||
|
android:height="8dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:pathData="M 0,1.9999996e-8 H 0.26458335 V 2.1166668 H 0 Z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_arrow_downward_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_delete_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_done_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_equalizer_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M10,20h4L14,4h-4v16zM4,20h4v-8L4,12v8zM16,9v11h4L20,9h-4z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_error_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_error_outline_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_get_app_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_link_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_list_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_music_note_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_settings_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_baseline_warning_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||||
|
</vector>
|
164
app/src/main/res/drawable/ic_deezer_logo.xml
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="1000dp"
|
||||||
|
android:height="191.2dp"
|
||||||
|
android:viewportWidth="1000"
|
||||||
|
android:viewportHeight="191.2">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M234.1,126.94c0,40.1 24.7,64.1 61.4,64.1c18.3,0 33.7,-5.1 42.3,-18.5v18.5h33.5L371.3,0.04h-34.8v81.2c-7.9,-13.4 -22.5,-19.4 -40.8,-19.4C259.9,61.74 234.1,86.24 234.1,126.94L234.1,126.94zM337.6,126.94c0,22.9 -15.6,37.2 -34.3,37.2c-19.4,0 -34.3,-14.3 -34.3,-37.2c0,-23.3 15,-37.9 34.3,-37.9C322,89.04 337.6,103.84 337.6,126.94z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M479.6,141.74c-4,14.8 -14.1,22.2 -30,22.2c-18.5,0 -33.7,-11.2 -34.1,-31h87.7c1.1,-4.9 1.6,-10.1 1.6,-15.8c0,-35.5 -24.2,-55.5 -59.9,-55.5c-38.1,0 -64.3,27.1 -64.3,63.9c0,41 28.9,65.6 68.9,65.6c30.2,0 50.7,-12.6 59.7,-37.7L479.6,141.74zM415.5,109.74C418.8,95.44 430.7,87.04 445,87.04c15.6,0 26.2,8.4 26.2,21.6l-0.2,1.1L415.5,109.74z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M608.7,141.74c-4,14.8 -14.1,22.2 -30,22.2c-18.5,0 -33.7,-11.2 -34.1,-31h87.7c1.1,-4.9 1.6,-10.1 1.6,-15.8c0,-35.5 -24.2,-55.5 -59.9,-55.5c-38.1,0 -64.3,27.1 -64.3,63.9c0,41 28.9,65.6 68.9,65.6c30.2,0 50.7,-12.6 59.7,-37.7L608.7,141.74zM544.6,109.74c3.3,-14.3 15.2,-22.7 29.5,-22.7c15.6,0 26.2,8.4 26.2,21.6l-0.2,1.1L544.6,109.74z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M756.7,191.24L756.7,159.74h-73.1l71.1,-69.2v-28.8L642.2,61.74v30h68.7L640,161.24v30L756.7,191.24z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M858.4,141.74c-4,14.8 -14.1,22.2 -30,22.2c-18.5,0 -33.7,-11.2 -34.1,-31L882,132.94c1.1,-4.9 1.6,-10.1 1.6,-15.8c0,-35.5 -24.2,-55.5 -59.9,-55.5c-38.1,0 -64.3,27.1 -64.3,63.9c0,41 28.9,65.6 68.9,65.6c30.2,0 50.7,-12.6 59.7,-37.7L858.4,141.74zM794.3,109.74c3.3,-14.3 15.2,-22.7 29.5,-22.7c15.6,0 26.2,8.4 26.2,21.6l-0.2,1.1L794.3,109.74z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/logo_text"
|
||||||
|
android:pathData="M966.1,106.44c0,1.3 0,3.7 0,3.7h33.9c0,0 0,-6.4 0,-9.9c0,-22.2 -13.9,-38.3 -37.2,-38.3c-15,0 -25.6,7.3 -31.1,19.4v-19.4h-35v129.3h35L931.7,107.94c0,-13.2 7.1,-20.5 18,-20.5C959.7,87.54 966.1,96.94 966.1,106.44z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M155.5,61.74h42.9v25.1h-42.9z"
|
||||||
|
android:fillColor="#40AB5D"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M155.5,96.54h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="121.860596"
|
||||||
|
android:startX="177.16011"
|
||||||
|
android:endY="96.17528"
|
||||||
|
android:endX="176.75674"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF358C7B"/>
|
||||||
|
<item android:offset="0.5256" android:color="#FF33A65E"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M155.5,131.34h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="155.15503"
|
||||||
|
android:startX="154.86931"
|
||||||
|
android:endY="132.64366"
|
||||||
|
android:endX="199.05013"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF222B90"/>
|
||||||
|
<item android:offset="1" android:color="#FF367B99"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,166.14h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="178.70355"
|
||||||
|
android:startX="0.007833929"
|
||||||
|
android:endY="178.70355"
|
||||||
|
android:endX="42.871952"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF9900"/>
|
||||||
|
<item android:offset="1" android:color="#FFFF8000"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M51.8,166.14h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="178.70355"
|
||||||
|
android:startX="51.847775"
|
||||||
|
android:endY="178.70355"
|
||||||
|
android:endX="94.71189"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFFF8000"/>
|
||||||
|
<item android:offset="1" android:color="#FFCC1953"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M103.7,166.14h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="178.70355"
|
||||||
|
android:startX="103.68771"
|
||||||
|
android:endY="178.70355"
|
||||||
|
android:endX="146.55183"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFCC1953"/>
|
||||||
|
<item android:offset="1" android:color="#FF241284"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M155.5,166.14h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="178.70355"
|
||||||
|
android:startX="155.47691"
|
||||||
|
android:endY="178.70355"
|
||||||
|
android:endX="198.34103"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF222B90"/>
|
||||||
|
<item android:offset="1" android:color="#FF3559A6"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M103.7,131.34h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="150.66125"
|
||||||
|
android:startX="101.995865"
|
||||||
|
android:endY="137.13744"
|
||||||
|
android:endX="148.24368"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FFCC1953"/>
|
||||||
|
<item android:offset="1" android:color="#FF241284"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M51.8,131.34h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="135.54341"
|
||||||
|
android:startX="50.322067"
|
||||||
|
android:endY="152.25546"
|
||||||
|
android:endX="96.2376"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.002669841" android:color="#FFFFCC00"/>
|
||||||
|
<item android:offset="0.9999" android:color="#FFCE1938"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:pathData="M51.8,96.54h42.9v25.1h-42.9z"
|
||||||
|
android:fillType="evenOdd">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="92.64302"
|
||||||
|
android:startX="55.450558"
|
||||||
|
android:endY="125.547455"
|
||||||
|
android:endX="91.10911"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.002669841" android:color="#FFFFD100"/>
|
||||||
|
<item android:offset="1" android:color="#FFFD5A22"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#111111"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
18
app/src/main/res/drawable/no_cover.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="19.05dp"
|
||||||
|
android:height="19.050001dp"
|
||||||
|
android:viewportWidth="19.05"
|
||||||
|
android:viewportHeight="19.050001">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#333333"
|
||||||
|
android:strokeWidth="1.32292"
|
||||||
|
android:strokeLineJoin="bevel"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M 0 -5e-7 H 19.050001 V 19.0500005 H 0 V -5e-7 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="0.529167"
|
||||||
|
android:pathData="M 9.5250003,4.7625001 V 10.345209 C 9.2127919,10.165292 8.8529586,10.054167 8.4666669,10.054167 c -1.1694584,0 -2.1166667,0.947208 -2.1166667,2.116667 0,1.169458 0.9472083,2.116666 2.1166667,2.116666 1.1694584,0 2.1166671,-0.947208 2.1166671,-2.116666 V 6.8791669 H 12.7 V 4.7625001 Z" />
|
||||||
|
</vector>
|
18
app/src/main/res/drawable/search_input.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:state_enabled="true" android:state_focused="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/secondary_background"/>
|
||||||
|
<corners android:radius="10dp"/>
|
||||||
|
<stroke android:color="@color/light_blue" android:width="2dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_enabled="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/secondary_background"/>
|
||||||
|
<corners android:radius="10dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
9
app/src/main/res/drawable/tag_background.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/dark_blue"/>
|
||||||
|
<corners android:radius="4dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
79
app/src/main/res/layout/activity_main.xml
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/rootLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settingsButton"
|
||||||
|
style="@android:style/Widget.Material.ImageButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/settings"
|
||||||
|
android:src="@drawable/ic_baseline_settings_24"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/downloadInputBox"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/downloadInputBox"
|
||||||
|
style="@style/Widget.AppCompat.AutoCompleteTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:background="@drawable/search_input"
|
||||||
|
android:drawableStart="@drawable/ic_baseline_link_24"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/downloadInputHint"
|
||||||
|
android:inputType="textUri"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/settingsButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/downloadQueue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/downloadInputBox" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/loadingScreen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
130
app/src/main/res/layout/download_item.xml
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:cardBackgroundColor="@color/cards_background"
|
||||||
|
app:cardCornerRadius="5dp"
|
||||||
|
app:cardElevation="1dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/constraintLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/trackBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:baselineAligned="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/coverImage"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:contentDescription="@string/cover_image"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/no_cover" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/qualityLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/tag_background"
|
||||||
|
android:padding="2dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="11sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/coverImage"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloadTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloadArtist"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:alpha="@dimen/secondary_alpha"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical|end"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="0dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloadProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/downloadFails"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/downloadBar"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:max="100"
|
||||||
|
android:progress="0"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/downloadStatus"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/trackBox" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/downloadStatus"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:foregroundTint="#505050"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/downloadBar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/trackBox"
|
||||||
|
app:srcCompat="@drawable/ic_baseline_list_24" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
88
app/src/main/res/layout/login_activity.xml
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="48dp"
|
||||||
|
android:layout_marginEnd="48dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_deezer_logo" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/emailField"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:hint="@string/prompt_email"
|
||||||
|
android:inputType="textEmailAddress"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:selectAllOnFocus="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/passwordField"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:hint="@string/prompt_password"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:selectAllOnFocus="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/emailField" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/loginButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:layout_marginStart="48dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="48dp"
|
||||||
|
android:layout_marginBottom="64dp"
|
||||||
|
android:enabled="true"
|
||||||
|
android:text="@string/action_sign_in"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/passwordField"
|
||||||
|
app:layout_constraintVertical_bias="0.2" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loginLoading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="32dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginBottom="64dp"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/passwordField"
|
||||||
|
app:layout_constraintHorizontal_bias="0.497"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/passwordField"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.9" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
72
app/src/main/res/layout/settings_activity.xml
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/loggedInSection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/userAvatar"
|
||||||
|
android:layout_width="115dp"
|
||||||
|
android:layout_height="115dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:contentDescription="@string/userAvatar_description"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/userName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/userAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/userInfo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/userAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/userName" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/logoutButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/logout_button"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/userAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/userInfo" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/preferencesFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 13 KiB |
12
app/src/main/res/values-night/colors.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="blue">#FF1976d2</color>
|
||||||
|
<color name="dark_blue">#FF004ba0</color>
|
||||||
|
<color name="light_blue">#ff63a4ff</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="primary_background">#1D1D1D</color>
|
||||||
|
<color name="secondary_background">#000000</color>
|
||||||
|
<color name="cards_background">#000000</color>
|
||||||
|
<color name="logo_text">#FFFFFF</color>
|
||||||
|
</resources>
|
18
app/src/main/res/values-night/themes.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.DeezerDownloader" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/blue</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/dark_blue</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/light_blue</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/blue</item>
|
||||||
|
<item name="colorOnSecondary">@color/white</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">@color/secondary_background</item>
|
||||||
|
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="cardBackgroundColor">@color/secondary_background</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
14
app/src/main/res/values/arrays.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<resources>
|
||||||
|
<!-- Reply Preference -->
|
||||||
|
<string-array name="download_quality_entries">
|
||||||
|
<item>FLAC</item>
|
||||||
|
<item>MP3 320kbps</item>
|
||||||
|
<item>MP3 128kbps</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="download_quality_values">
|
||||||
|
<item>9</item>
|
||||||
|
<item>3</item>
|
||||||
|
<item>1</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
12
app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="blue">#FF1976d2</color>
|
||||||
|
<color name="dark_blue">#FF004ba0</color>
|
||||||
|
<color name="light_blue">#ff63a4ff</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="primary_background">#FFFFFF</color>
|
||||||
|
<color name="secondary_background">#ededed</color>
|
||||||
|
<color name="cards_background">#FFFFFF</color>
|
||||||
|
<color name="logo_text">#000000</color>
|
||||||
|
</resources>
|
4
app/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="secondary_alpha" type="dimen" format="float">0.57</item>
|
||||||
|
</resources>
|
25
app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Deezer Downloader</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="downloadInputHint">URL...</string>
|
||||||
|
<string name="title_activity_settings">SettingsActivity</string>
|
||||||
|
|
||||||
|
<!-- Preference Titles -->
|
||||||
|
<string name="account_header">Account</string>
|
||||||
|
<string name="download_header">Downloads</string>
|
||||||
|
|
||||||
|
<!-- Account Preferences -->
|
||||||
|
<string name="logout_button">Logout</string>
|
||||||
|
|
||||||
|
<!-- Download Preferences -->
|
||||||
|
<string name="download_quality">Download quality</string>
|
||||||
|
<string name="download_quality_description">Choose at which quality to download the tracks</string>
|
||||||
|
|
||||||
|
<!-- Strings related to login -->
|
||||||
|
<string name="prompt_email">Email</string>
|
||||||
|
<string name="prompt_password">Password</string>
|
||||||
|
<string name="action_sign_in">Log in</string>
|
||||||
|
<string name="title_activity_login">Login</string>
|
||||||
|
<string name="cover_image">Cover Image</string>
|
||||||
|
<string name="userAvatar_description">User\'s Avatar</string>
|
||||||
|
</resources>
|
17
app/src/main/res/values/themes.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.DeezerDownloader" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/blue</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/dark_blue</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/light_blue</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/blue</item>
|
||||||
|
<item name="colorOnSecondary">@color/white</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">@color/secondary_background</item>
|
||||||
|
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
15
app/src/main/res/xml/preferences.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<PreferenceCategory
|
||||||
|
android:icon="@drawable/ic_baseline_get_app_24"
|
||||||
|
android:title="@string/download_header">
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="1"
|
||||||
|
android:entries="@array/download_quality_entries"
|
||||||
|
android:entryValues="@array/download_quality_values"
|
||||||
|
android:icon="@drawable/ic_baseline_equalizer_24"
|
||||||
|
android:key="download_quality"
|
||||||
|
android:summary="@string/download_quality_description"
|
||||||
|
android:title="@string/download_quality" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
</PreferenceScreen>
|
17
app/src/test/java/app/deemix/downloader/ExampleUnitTest.kt
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package app.deemix.downloader
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
10
build.gradle
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '7.2.1' apply false
|
||||||
|
id 'com.android.library' version '7.2.1' apply false
|
||||||
|
id 'org.jetbrains.kotlin.android' version '1.6.20' apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
23
gradle.properties
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
|
# thereby reducing the size of the R class for that library
|
||||||
|
android.nonTransitiveRClass=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Sun Mar 13 21:48:14 CET 2022
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
185
gradlew
vendored
Executable file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
16
settings.gradle
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "Deezer Downloader"
|
||||||
|
include ':app'
|