feat: add HTTP stream connection with ICY header support
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package xyz.cottongin.radio247.audio
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.time.Duration
|
||||
|
||||
class ConnectionFailed(message: String, cause: Throwable? = null) : Exception(message, cause)
|
||||
|
||||
class StreamConnection(private val url: String) {
|
||||
private val client = OkHttpClient.Builder()
|
||||
.readTimeout(Duration.ofSeconds(30))
|
||||
.build()
|
||||
|
||||
var metaint: Int? = null
|
||||
private set
|
||||
var inputStream: InputStream? = null
|
||||
private set
|
||||
private var response: Response? = null
|
||||
|
||||
fun open() {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.header("Icy-MetaData", "1")
|
||||
.header("User-Agent", "Radio247/1.0")
|
||||
.build()
|
||||
|
||||
try {
|
||||
val resp = client.newCall(request).execute()
|
||||
if (!resp.isSuccessful) {
|
||||
resp.close()
|
||||
throw ConnectionFailed("HTTP ${resp.code}")
|
||||
}
|
||||
response = resp
|
||||
metaint = resp.header("icy-metaint")?.toIntOrNull()
|
||||
inputStream = resp.body?.byteStream()
|
||||
?: throw ConnectionFailed("Empty response body")
|
||||
} catch (e: IOException) {
|
||||
throw ConnectionFailed("Network error", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
try {
|
||||
inputStream?.close()
|
||||
} catch (_: IOException) {}
|
||||
try {
|
||||
response?.close()
|
||||
} catch (_: IOException) {}
|
||||
response = null
|
||||
inputStream = null
|
||||
metaint = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package xyz.cottongin.radio247.audio
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
|
||||
class StreamConnectionTest {
|
||||
|
||||
private val server = MockWebServer()
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sends_icy_metadata_header_and_reads_metaint_from_response() {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("icy-metaint", "16000")
|
||||
.setHeader("Content-Type", "audio/mpeg")
|
||||
.setBody("fake audio data")
|
||||
)
|
||||
server.start()
|
||||
|
||||
val conn = StreamConnection(server.url("/stream").toString())
|
||||
conn.open()
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertEquals("1", request.getHeader("Icy-MetaData"))
|
||||
assertEquals(16000, conn.metaint)
|
||||
assertNotNull(conn.inputStream)
|
||||
|
||||
conn.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun metaint_is_null_when_server_does_not_provide_it() {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("Content-Type", "audio/mpeg")
|
||||
.setBody("fake audio data")
|
||||
)
|
||||
server.start()
|
||||
|
||||
val conn = StreamConnection(server.url("/stream").toString())
|
||||
conn.open()
|
||||
|
||||
assertNull(conn.metaint)
|
||||
assertNotNull(conn.inputStream)
|
||||
|
||||
conn.close()
|
||||
}
|
||||
|
||||
@Test(expected = ConnectionFailed::class)
|
||||
fun throws_ConnectionFailed_on_HTTP_error() {
|
||||
server.enqueue(MockResponse().setResponseCode(404))
|
||||
server.start()
|
||||
|
||||
val conn = StreamConnection(server.url("/stream").toString())
|
||||
conn.open()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun throws_ConnectionFailed_on_network_error() {
|
||||
server.start()
|
||||
val url = server.url("/stream").toString()
|
||||
server.shutdown()
|
||||
|
||||
try {
|
||||
val conn = StreamConnection(url)
|
||||
conn.open()
|
||||
fail("Expected ConnectionFailed")
|
||||
} catch (e: ConnectionFailed) {
|
||||
assertTrue(e.message!!.contains("Network error"))
|
||||
assertNotNull(e.cause)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun close_cleans_up_resources() {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("icy-metaint", "16000")
|
||||
.setBody("fake audio data")
|
||||
)
|
||||
server.start()
|
||||
|
||||
val conn = StreamConnection(server.url("/stream").toString())
|
||||
conn.open()
|
||||
assertNotNull(conn.inputStream)
|
||||
assertNotNull(conn.metaint)
|
||||
|
||||
conn.close()
|
||||
|
||||
assertNull(conn.inputStream)
|
||||
assertNull(conn.metaint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sends_user_agent_header() {
|
||||
server.enqueue(
|
||||
MockResponse()
|
||||
.setHeader("icy-metaint", "16000")
|
||||
.setBody("fake audio data")
|
||||
)
|
||||
server.start()
|
||||
|
||||
val conn = StreamConnection(server.url("/stream").toString())
|
||||
conn.open()
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertEquals("Radio247/1.0", request.getHeader("User-Agent"))
|
||||
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user