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'
|