ultrasonic-app-subsonic-and.../subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt

113 lines
4.0 KiB
Kotlin

package org.moire.ultrasonic.api.subsonic
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import java.lang.IllegalStateException
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
/**
* Subsonic API client that provides api access.
*
* For supported API calls see [SubsonicAPIDefinition].
*
* @author Yahor Berdnikau
*/
class SubsonicAPIClient(baseUrl: String,
username: String,
private val password: String,
clientProtocolVersion: SubsonicAPIVersions,
clientID: String,
debug: Boolean = false) {
companion object {
internal val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
// Adds default request params
val originalRequest = chain.request()
val newUrl = originalRequest.url().newBuilder()
.addQueryParameter("u", username)
.also {
it.addPasswordQueryParam(clientProtocolVersion)
}
.addQueryParameter("v", clientProtocolVersion.restApiVersion)
.addQueryParameter("c", clientID)
.addQueryParameter("f", "json")
.build()
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
}
.also {
if (debug) {
it.addLogging()
}
}.build()
private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
val api: SubsonicAPIDefinition = retrofit.create(SubsonicAPIDefinition::class.java)
private val salt: String by lazy {
val secureRandom = SecureRandom()
BigInteger(130, secureRandom).toString(32)
}
private val passwordMD5Hash: String by lazy {
try {
val md5Digest = MessageDigest.getInstance("MD5")
md5Digest.digest("$password$salt".toByteArray()).toHexBytes()
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException(e)
}
}
private val passwordHex: String by lazy {
"enc:${password.toHexBytes()}"
}
private fun String.toHexBytes(): String {
return this.toByteArray().toHexBytes()
}
private fun ByteArray.toHexBytes(): String {
val hexChars = CharArray(this.size * 2)
for (j in 0..this.lastIndex) {
val v = this[j].toInt().and(0xFF)
hexChars[j * 2] = HEX_ARRAY[v.ushr(4)]
hexChars[j * 2 + 1] = HEX_ARRAY[v.and(0x0F)]
}
return String(hexChars)
}
private fun OkHttpClient.Builder.addLogging() {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC
this.addInterceptor(loggingInterceptor)
}
private fun HttpUrl.Builder.addPasswordQueryParam(clientProtocolVersion: SubsonicAPIVersions) {
if (clientProtocolVersion < SubsonicAPIVersions.V1_13_0) {
this.addQueryParameter("p", passwordHex)
} else {
this.addQueryParameter("t", passwordMD5Hash)
this.addQueryParameter("s", salt)
}
}
}