Merge pull request #525 from tzugen/fix-video-url

Properly generate the Video stream url, without actually making a request
This commit is contained in:
Nite 2021-06-16 10:05:50 +02:00 committed by GitHub
commit 2d4c773344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 36 additions and 99 deletions

View File

@ -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
}
}

View File

@ -83,19 +83,3 @@ fun StreamResponse.throwOnFailure(): StreamResponse {
} }
return this 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
}

View File

@ -62,7 +62,7 @@ class SubsonicAPIClient(
onProtocolChange(field) onProtocolChange(field)
} }
private val okHttpClient = baseOkClient.newBuilder() val okHttpClient: OkHttpClient = baseOkClient.newBuilder()
.readTimeout(READ_TIMEOUT, MILLISECONDS) .readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() } .apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
.addInterceptor { chain -> .addInterceptor { chain ->
@ -83,7 +83,7 @@ class SubsonicAPIClient(
// Create the Retrofit instance, and register a special converter factory // 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 // 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/") .baseUrl("${config.baseUrl}/rest/")
.client(okHttpClient) .client(okHttpClient)
.addConverterFactory( .addConverterFactory(

View File

@ -11,12 +11,11 @@ import java.io.File
import java.io.FileWriter import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.CountDownLatch import okhttp3.Protocol
import java.util.concurrent.TimeUnit import okhttp3.Response
import java.util.concurrent.TimeoutException import okhttp3.ResponseBody
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient 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.AlbumListType.Companion.fromName
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.throwOnFailure
@ -53,7 +52,7 @@ import timber.log.Timber
*/ */
@Suppress("LargeClass") @Suppress("LargeClass")
open class RESTMusicService( open class RESTMusicService(
subsonicAPIClient: SubsonicAPIClient, val subsonicAPIClient: SubsonicAPIClient,
private val fileStorage: PermanentFileStorage, private val fileStorage: PermanentFileStorage,
private val activeServerProvider: ActiveServerProvider private val activeServerProvider: ActiveServerProvider
) : MusicService { ) : MusicService {
@ -479,35 +478,43 @@ open class RESTMusicService(
return Pair(response.stream!!, partial) 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) @Throws(Exception::class)
override fun getVideoUrl( override fun getVideoUrl(
id: String id: String
): String { ): String {
// TODO This method should not exists as video should be loaded using stream method // Create a new modified okhttp client to intercept the URL
// Previous method implementation uses assumption that video will be available val builder = subsonicAPIClient.okHttpClient.newBuilder()
// by videoPlayer.view?id=<id>&maxBitRate=500&autoplay=true, but this url is not
// official Subsonic API call.
val expectedResult = arrayOfNulls<String>(1)
expectedResult[0] = null
val latch = CountDownLatch(1) builder.addInterceptor { chain ->
// Returns a dummy response
Thread( Response.Builder()
{ .code(100)
expectedResult[0] = API.getStreamUrl(id) .body(ResponseBody.create(null, ""))
latch.countDown() .protocol(Protocol.HTTP_2)
}, .message("Empty response")
"Get-Video-Url" .request(chain.request())
).start() .build()
// 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")
} }
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) @Throws(Exception::class)