Add using new authentication method since 1.13.0.
Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
parent
20d95ce19d
commit
f1ab0a3e0c
|
@ -12,11 +12,12 @@ ext.versions = [
|
||||||
|
|
||||||
retrofit : "2.1.0",
|
retrofit : "2.1.0",
|
||||||
jackson : "2.8.7",
|
jackson : "2.8.7",
|
||||||
|
okhttp : "3.6.0",
|
||||||
|
|
||||||
junit : "4.12",
|
junit : "4.12",
|
||||||
mockitoKotlin : "1.3.0",
|
mockitoKotlin : "1.5.0",
|
||||||
kluent : "1.15",
|
kluent : "1.26",
|
||||||
okhttp : "3.6.0",
|
apacheCodecs : "1.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.gradlePlugins = [
|
ext.gradlePlugins = [
|
||||||
|
@ -30,7 +31,7 @@ ext.androidSupport = [
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.other = [
|
ext.other = [
|
||||||
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-common:$versions.kotlin",
|
kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlin",
|
||||||
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
|
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
|
||||||
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
|
gsonConverter : "com.squareup.retrofit2:converter-gson:$versions.retrofit",
|
||||||
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
||||||
|
@ -41,7 +42,9 @@ ext.other = [
|
||||||
ext.testing = [
|
ext.testing = [
|
||||||
junit : "junit:junit:$versions.junit",
|
junit : "junit:junit:$versions.junit",
|
||||||
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
|
kotlinJunit : "org.jetbrains.kotlin:kotlin-test-junit:$versions.kotlin",
|
||||||
|
kotlinReflect : "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin",
|
||||||
mockitoKotlin : "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin",
|
mockitoKotlin : "com.nhaarman:mockito-kotlin:$versions.mockitoKotlin",
|
||||||
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
kluent : "org.amshove.kluent:kluent:$versions.kluent",
|
||||||
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp"
|
mockWebServer : "com.squareup.okhttp3:mockwebserver:$versions.okhttp",
|
||||||
|
apacheCodecs : "commons-codec:commons-codec:$versions.apacheCodecs",
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,7 +16,9 @@ dependencies {
|
||||||
|
|
||||||
testCompile testing.junit
|
testCompile testing.junit
|
||||||
testCompile testing.kotlinJunit
|
testCompile testing.kotlinJunit
|
||||||
|
testCompile testing.kotlinReflect
|
||||||
testCompile testing.mockitoKotlin
|
testCompile testing.mockitoKotlin
|
||||||
testCompile testing.kluent
|
testCompile testing.kluent
|
||||||
testCompile testing.mockWebServer
|
testCompile testing.mockWebServer
|
||||||
|
testCompile testing.apacheCodecs
|
||||||
}
|
}
|
|
@ -6,21 +6,28 @@ import org.amshove.kluent.`should be`
|
||||||
import org.amshove.kluent.`should contain`
|
import org.amshove.kluent.`should contain`
|
||||||
import org.amshove.kluent.`should equal`
|
import org.amshove.kluent.`should equal`
|
||||||
import org.amshove.kluent.`should not be`
|
import org.amshove.kluent.`should not be`
|
||||||
|
import org.amshove.kluent.`should not contain`
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.moire.ultrasonic.api.subsonic.models.*
|
import org.moire.ultrasonic.api.subsonic.models.Artist
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.Index
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.License
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.MusicFolder
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||||
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
import java.security.MessageDigest
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration test for [SubsonicAPI] class.
|
* Integration test for [SubsonicAPIClient] class.
|
||||||
*/
|
*/
|
||||||
class SubsonicAPITest {
|
class SubsonicAPIClientTest {
|
||||||
companion object {
|
companion object {
|
||||||
const val USERNAME = "some-user"
|
const val USERNAME = "some-user"
|
||||||
const val PASSWORD = "some-password"
|
const val PASSWORD = "some-password"
|
||||||
|
@ -30,19 +37,58 @@ class SubsonicAPITest {
|
||||||
|
|
||||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
private lateinit var api: SubsonicAPI
|
private lateinit var client: SubsonicAPIClient
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
api = SubsonicAPI(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD,
|
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME, PASSWORD,
|
||||||
CLIENT_VERSION, CLIENT_ID)
|
CLIENT_VERSION, CLIENT_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Should pass password hash and salt in query params for api version 1_13_0`() {
|
||||||
|
val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME,
|
||||||
|
PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID)
|
||||||
|
enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
|
clientV12.api.ping().execute()
|
||||||
|
|
||||||
|
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
||||||
|
requestLine `should contain` "&s="
|
||||||
|
requestLine `should contain` "&t="
|
||||||
|
requestLine `should not contain` "&p=enc:"
|
||||||
|
|
||||||
|
val salt = requestLine.split('&').find { it.startsWith("s=") }?.substringAfter('=')
|
||||||
|
val token = requestLine.split('&').find { it.startsWith("t=") }?.substringAfter('=')
|
||||||
|
val expectedToken = String(Hex.encodeHex(MessageDigest.getInstance("MD5")
|
||||||
|
.digest("$PASSWORD$salt".toByteArray()), false))
|
||||||
|
token!! `should equal` expectedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Should pass hex encoded password in query params for api version 1_12_0`() {
|
||||||
|
val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(), USERNAME,
|
||||||
|
PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID)
|
||||||
|
enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
|
clientV11.api.ping().execute()
|
||||||
|
|
||||||
|
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
||||||
|
requestLine `should not contain` "&s="
|
||||||
|
requestLine `should not contain` "&t="
|
||||||
|
requestLine `should contain` "&p=enc:"
|
||||||
|
val passParam = requestLine.split('&').find { it.startsWith("p=enc:") }
|
||||||
|
val encodedPassword = String(Hex.encodeHex(PASSWORD.toByteArray(), false))
|
||||||
|
passParam `should equal` "p=enc:$encodedPassword"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse ping ok response`() {
|
fun `Should parse ping ok response`() {
|
||||||
enqueueResponse("ping_ok.json")
|
enqueueResponse("ping_ok.json")
|
||||||
|
|
||||||
val response = api.getApi().ping().execute()
|
val response = client.api.ping().execute()
|
||||||
|
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
with(response.body()) {
|
with(response.body()) {
|
||||||
|
@ -52,14 +98,14 @@ class SubsonicAPITest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse ping error response`() {
|
fun `Should parse ping error response`() {
|
||||||
checkErrorCallParsed { api.getApi().ping().execute() }
|
checkErrorCallParsed { client.api.ping().execute() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse get license ok response`() {
|
fun `Should parse get license ok response`() {
|
||||||
enqueueResponse("license_ok.json")
|
enqueueResponse("license_ok.json")
|
||||||
|
|
||||||
val response = api.getApi().getLicense().execute()
|
val response = client.api.getLicense().execute()
|
||||||
|
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
with(response.body()) {
|
with(response.body()) {
|
||||||
|
@ -70,7 +116,7 @@ class SubsonicAPITest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse get license error response`() {
|
fun `Should parse get license error response`() {
|
||||||
val response = checkErrorCallParsed { api.getApi().getLicense().execute() }
|
val response = checkErrorCallParsed { client.api.getLicense().execute() }
|
||||||
|
|
||||||
response.license `should be` null
|
response.license `should be` null
|
||||||
}
|
}
|
||||||
|
@ -79,7 +125,7 @@ class SubsonicAPITest {
|
||||||
fun `Should parse get music folders ok response`() {
|
fun `Should parse get music folders ok response`() {
|
||||||
enqueueResponse("get_music_folders_ok.json")
|
enqueueResponse("get_music_folders_ok.json")
|
||||||
|
|
||||||
val response = api.getApi().getMusicFolders().execute()
|
val response = client.api.getMusicFolders().execute()
|
||||||
|
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
with(response.body()) {
|
with(response.body()) {
|
||||||
|
@ -90,7 +136,7 @@ class SubsonicAPITest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse get music folders error response`() {
|
fun `Should parse get music folders error response`() {
|
||||||
val response = checkErrorCallParsed { api.getApi().getMusicFolders().execute() }
|
val response = checkErrorCallParsed { client.api.getMusicFolders().execute() }
|
||||||
|
|
||||||
response.musicFolders `should be` null
|
response.musicFolders `should be` null
|
||||||
}
|
}
|
||||||
|
@ -100,7 +146,7 @@ class SubsonicAPITest {
|
||||||
// TODO: check for shortcut parsing
|
// TODO: check for shortcut parsing
|
||||||
enqueueResponse("get_indexes_ok.json")
|
enqueueResponse("get_indexes_ok.json")
|
||||||
|
|
||||||
val response = api.getApi().getIndexes(null, null).execute()
|
val response = client.api.getIndexes(null, null).execute()
|
||||||
|
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
response.body().indexes `should not be` null
|
response.body().indexes `should not be` null
|
||||||
|
@ -126,7 +172,7 @@ class SubsonicAPITest {
|
||||||
enqueueResponse("get_indexes_ok.json")
|
enqueueResponse("get_indexes_ok.json")
|
||||||
val musicFolderId = 9L
|
val musicFolderId = 9L
|
||||||
|
|
||||||
api.getApi().getIndexes(musicFolderId, null).execute()
|
client.api.getIndexes(musicFolderId, null).execute()
|
||||||
|
|
||||||
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
||||||
requestLine `should contain` "musicFolderId=$musicFolderId"
|
requestLine `should contain` "musicFolderId=$musicFolderId"
|
||||||
|
@ -138,7 +184,7 @@ class SubsonicAPITest {
|
||||||
enqueueResponse("get_indexes_ok.json")
|
enqueueResponse("get_indexes_ok.json")
|
||||||
val ifModifiedSince = System.currentTimeMillis()
|
val ifModifiedSince = System.currentTimeMillis()
|
||||||
|
|
||||||
api.getApi().getIndexes(null, ifModifiedSince).execute()
|
client.api.getIndexes(null, ifModifiedSince).execute()
|
||||||
|
|
||||||
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
||||||
requestLine `should contain` "ifModifiedSince=$ifModifiedSince"
|
requestLine `should contain` "ifModifiedSince=$ifModifiedSince"
|
||||||
|
@ -151,7 +197,7 @@ class SubsonicAPITest {
|
||||||
val musicFolderId = 110L
|
val musicFolderId = 110L
|
||||||
val ifModifiedSince = System.currentTimeMillis()
|
val ifModifiedSince = System.currentTimeMillis()
|
||||||
|
|
||||||
api.getApi().getIndexes(musicFolderId, ifModifiedSince).execute()
|
client.api.getIndexes(musicFolderId, ifModifiedSince).execute()
|
||||||
|
|
||||||
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
with(mockWebServerRule.mockWebServer.takeRequest()) {
|
||||||
requestLine `should contain` "musicFolderId=$musicFolderId"
|
requestLine `should contain` "musicFolderId=$musicFolderId"
|
||||||
|
@ -161,14 +207,14 @@ class SubsonicAPITest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse get indexes error response`() {
|
fun `Should parse get indexes error response`() {
|
||||||
val response = checkErrorCallParsed { api.getApi().getIndexes(null, null).execute() }
|
val response = checkErrorCallParsed { client.api.getIndexes(null, null).execute() }
|
||||||
|
|
||||||
response.indexes `should be` null
|
response.indexes `should be` null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Should parse getMusicDirectory error response`() {
|
fun `Should parse getMusicDirectory error response`() {
|
||||||
val response = checkErrorCallParsed { api.getApi().getMusicDirectory(1).execute() }
|
val response = checkErrorCallParsed { client.api.getMusicDirectory(1).execute() }
|
||||||
|
|
||||||
response.musicDirectory `should be` null
|
response.musicDirectory `should be` null
|
||||||
}
|
}
|
||||||
|
@ -178,7 +224,7 @@ class SubsonicAPITest {
|
||||||
enqueueResponse("get_music_directory_ok.json")
|
enqueueResponse("get_music_directory_ok.json")
|
||||||
val directoryId = 124L
|
val directoryId = 124L
|
||||||
|
|
||||||
api.getApi().getMusicDirectory(directoryId).execute()
|
client.api.getMusicDirectory(directoryId).execute()
|
||||||
|
|
||||||
mockWebServerRule.mockWebServer.takeRequest().requestLine `should contain` "id=$directoryId"
|
mockWebServerRule.mockWebServer.takeRequest().requestLine `should contain` "id=$directoryId"
|
||||||
}
|
}
|
||||||
|
@ -187,7 +233,7 @@ class SubsonicAPITest {
|
||||||
fun `Should parse get music directory ok response`() {
|
fun `Should parse get music directory ok response`() {
|
||||||
enqueueResponse("get_music_directory_ok.json")
|
enqueueResponse("get_music_directory_ok.json")
|
||||||
|
|
||||||
val response = api.getApi().getMusicDirectory(1).execute()
|
val response = client.api.getMusicDirectory(1).execute()
|
||||||
|
|
||||||
assertResponseSuccessful(response)
|
assertResponseSuccessful(response)
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
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.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.jackson.JacksonConverterFactory
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main entry for Subsonic API calls.
|
|
||||||
*
|
|
||||||
* @see SubsonicAPI http://www.subsonic.org/pages/api.jsp
|
|
||||||
*/
|
|
||||||
class SubsonicAPI(baseUrl: String,
|
|
||||||
username: String,
|
|
||||||
private val password: String,
|
|
||||||
clientProtocolVersion: SubsonicAPIVersions,
|
|
||||||
clientID: String,
|
|
||||||
debug: Boolean = false) {
|
|
||||||
private val okHttpClient = OkHttpClient.Builder()
|
|
||||||
.addInterceptor { chain ->
|
|
||||||
// Adds default request params
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
val newUrl = originalRequest.url().newBuilder()
|
|
||||||
.addQueryParameter("u", username)
|
|
||||||
.addQueryParameter("p", passwordHex())
|
|
||||||
.addQueryParameter("v", clientProtocolVersion.restApiVersion)
|
|
||||||
.addQueryParameter("c", clientID)
|
|
||||||
.addQueryParameter("f", "json")
|
|
||||||
.build()
|
|
||||||
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
|
|
||||||
}
|
|
||||||
.apply {
|
|
||||||
if (debug) {
|
|
||||||
val loggingInterceptor = HttpLoggingInterceptor()
|
|
||||||
loggingInterceptor.level = HttpLoggingInterceptor.Level.BASIC
|
|
||||||
this.addInterceptor(loggingInterceptor)
|
|
||||||
}
|
|
||||||
}.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()
|
|
||||||
|
|
||||||
private val subsonicAPI = retrofit.create(SubsonicAPIDefinition::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get API instance.
|
|
||||||
*
|
|
||||||
* @return initialized API instance
|
|
||||||
*/
|
|
||||||
fun getApi(): SubsonicAPIDefinition = subsonicAPI
|
|
||||||
|
|
||||||
private fun passwordHex() = "enc:${password.toHexBytes()}"
|
|
||||||
|
|
||||||
private fun String.toHexBytes(): String {
|
|
||||||
return String.format("%040x", BigInteger(1, this.toByteArray()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,8 +40,10 @@ dependencies {
|
||||||
}
|
}
|
||||||
testCompile(testing.mockitoKotlin) {
|
testCompile(testing.mockitoKotlin) {
|
||||||
exclude module: "kotlin-stdlib"
|
exclude module: "kotlin-stdlib"
|
||||||
|
exclude module: "kotlin-reflect"
|
||||||
}
|
}
|
||||||
testCompile(testing.kluent) {
|
testCompile(testing.kluent) {
|
||||||
exclude module: "kotlin-stdlib"
|
exclude module: "kotlin-stdlib"
|
||||||
|
exclude module: "kotlin-reflect"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue