From 57d740af1237d414f3c24a24ebb1d909a54db13f Mon Sep 17 00:00:00 2001 From: tzugen Date: Mon, 14 Jun 2021 20:31:53 +0200 Subject: [PATCH] Properly generate the Video stream url, without actually making a request! --- .../api/subsonic/GetStreamUrlTest.kt | 54 ---------------- .../ultrasonic/api/subsonic/Extensions.kt | 16 ----- .../api/subsonic/SubsonicAPIClient.kt | 4 +- .../ultrasonic/service/RESTMusicService.kt | 61 +++++++++++-------- 4 files changed, 36 insertions(+), 99 deletions(-) delete mode 100644 core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt diff --git a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt b/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt deleted file mode 100644 index 7cdf4a69..00000000 --- a/core/subsonic-api/src/integrationTest/kotlin/org/moire/ultrasonic/api/subsonic/GetStreamUrlTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.moire.ultrasonic.api.subsonic - -import okhttp3.mockwebserver.MockResponse -import org.amshove.kluent.`should be equal to` -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions.V1_6_0 -import org.moire.ultrasonic.api.subsonic.interceptors.toHexBytes -import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule - -/** - * Integration test for [getStreamUrl] method. - */ -class GetStreamUrlTest { - @JvmField @Rule val mockWebServerRule = MockWebServerRule() - - val id = "boom" - private lateinit var client: SubsonicAPIClient - private lateinit var expectedUrl: String - - @Before - fun setUp() { - val config = SubsonicClientConfiguration( - mockWebServerRule.mockWebServer.url("/").toString(), - USERNAME, - PASSWORD, - V1_6_0, - CLIENT_ID - ) - client = SubsonicAPIClient(config) - val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString() - expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&format=raw&u=$USERNAME" + - "&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}" - } - - @Test - fun `Should return valid stream url`() { - mockWebServerRule.enqueueResponse("ping_ok.json") - - val streamUrl = client.api.getStreamUrl(id) - - streamUrl `should be equal to` expectedUrl - } - - @Test - fun `Should still return stream url if connection failed`() { - mockWebServerRule.mockWebServer.enqueue(MockResponse().setResponseCode(500)) - - val streamUrl = client.api.getStreamUrl(id) - - streamUrl `should be equal to` expectedUrl - } -} diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt index 666dbcf1..f9d48d23 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/Extensions.kt @@ -83,19 +83,3 @@ fun StreamResponse.throwOnFailure(): StreamResponse { } return this } - -/** - * Gets a stream url. - * - * Calling this method do actual connection to the backend, though not downloading all content. - * - * Consider do not use this method, but [SubsonicAPIDefinition.stream] call. - */ -fun SubsonicAPIDefinition.getStreamUrl(id: String): String { - val response = this.stream(id, format = "raw").execute() - val url = response.raw().request().url().toString() - if (response.isSuccessful) { - response.body()?.close() - } - return url -} diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt index c50cc353..b5847c95 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIClient.kt @@ -62,7 +62,7 @@ class SubsonicAPIClient( onProtocolChange(field) } - private val okHttpClient = baseOkClient.newBuilder() + val okHttpClient: OkHttpClient = baseOkClient.newBuilder() .readTimeout(READ_TIMEOUT, MILLISECONDS) .apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() } .addInterceptor { chain -> @@ -83,7 +83,7 @@ class SubsonicAPIClient( // Create the Retrofit instance, and register a special converter factory // It will update our protocol version to the correct version, once we made a successful call - private val retrofit = Retrofit.Builder() + val retrofit: Retrofit = Retrofit.Builder() .baseUrl("${config.baseUrl}/rest/") .client(okHttpClient) .addConverterFactory( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 6d3a531c..4876bd9e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -11,12 +11,11 @@ import java.io.File import java.io.FileWriter import java.io.IOException import java.io.InputStream -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient -import org.moire.ultrasonic.api.subsonic.getStreamUrl import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.throwOnFailure @@ -53,7 +52,7 @@ import timber.log.Timber */ @Suppress("LargeClass") open class RESTMusicService( - subsonicAPIClient: SubsonicAPIClient, + val subsonicAPIClient: SubsonicAPIClient, private val fileStorage: PermanentFileStorage, private val activeServerProvider: ActiveServerProvider ) : MusicService { @@ -479,35 +478,43 @@ open class RESTMusicService( return Pair(response.stream!!, partial) } + /** + * We currently don't handle video playback in the app, but just create an Intent which video + * players can respond to. For this intent we need the full URL of the stream, including the + * authentication params. This is a bit tricky, because we want to avoid actually executing the + * call because that could take a long time. + */ @Throws(Exception::class) override fun getVideoUrl( id: String ): String { - // TODO This method should not exists as video should be loaded using stream method - // Previous method implementation uses assumption that video will be available - // by videoPlayer.view?id=&maxBitRate=500&autoplay=true, but this url is not - // official Subsonic API call. - val expectedResult = arrayOfNulls(1) - expectedResult[0] = null + // Create a new modified okhttp client to intercept the URL + val builder = subsonicAPIClient.okHttpClient.newBuilder() - val latch = CountDownLatch(1) - - Thread( - { - expectedResult[0] = API.getStreamUrl(id) - latch.countDown() - }, - "Get-Video-Url" - ).start() - - // Getting the stream can take a long time on some servers - latch.await(1, TimeUnit.MINUTES) - - if (expectedResult[0] == null) { - throw TimeoutException("Server didn't respond in time") + builder.addInterceptor { chain -> + // Returns a dummy response + Response.Builder() + .code(100) + .body(ResponseBody.create(null, "")) + .protocol(Protocol.HTTP_2) + .message("Empty response") + .request(chain.request()) + .build() } - return expectedResult[0]!! + // Create a new Okhttp client + val client = builder.build() + + // Get the request from Retrofit, but don't execute it! + val request = API.stream(id, format = "raw").request() + + // Create a new call with the request, and execute ist on our custom client + val response = client.newCall(request).execute() + + // The complete url :) + val url = response.request().url() + + return url.toString() } @Throws(Exception::class)