Improve REST layer user experience
This commit is contained in:
@@ -1,15 +1,8 @@
|
||||
package io.bartek.ttsserver.core.web.dto
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
data class BaseDTO(val text: String, val language: Locale) : DTO() {
|
||||
companion object {
|
||||
fun fromJSON(json: String) = JSONObject(json).let {root ->
|
||||
val language = root.nullableString("language") ?.let { Locale(it) } ?: Locale.US
|
||||
val text = root.requiredString("text")
|
||||
|
||||
BaseDTO(text, language)
|
||||
}
|
||||
}
|
||||
class BaseDTO(json: String) : DTO(json) {
|
||||
val language = nullableObject("language", Locale.US, { Locale(it) }, { it.toString() })
|
||||
val text = requiredString("text")
|
||||
}
|
||||
@@ -1,16 +1,27 @@
|
||||
package io.bartek.ttsserver.core.web.dto
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.Response
|
||||
import fi.iki.elonen.NanoHTTPD.ResponseException
|
||||
import io.bartek.ttsserver.core.web.exception.WebException
|
||||
import org.json.JSONObject
|
||||
|
||||
abstract class DTO {
|
||||
companion object {
|
||||
fun JSONObject.requiredString(key: String) = this.nullableString(key)
|
||||
?: throw ResponseException(Response.Status.BAD_REQUEST, "")
|
||||
abstract class DTO(json: String) : JSONObject(json) {
|
||||
val json: String
|
||||
get() = toString()
|
||||
|
||||
protected fun requiredString(key: String) = this.optString(key)
|
||||
.takeIf { it.isNotBlank() }
|
||||
?: throw WebException(Response.Status.BAD_REQUEST, "The '$key' field is required")
|
||||
|
||||
fun JSONObject.nullableString(key: String) = this.optString(key)
|
||||
.takeIf { it.isNotBlank() }
|
||||
}
|
||||
protected fun nullableInt(key: String, default: Int) = this.optInt(key, default)
|
||||
.also { put(key, it) }
|
||||
|
||||
protected fun <T> nullableObject(
|
||||
key: String,
|
||||
default: T,
|
||||
deserializer: (String) -> T,
|
||||
serializer: (T) -> String
|
||||
): T = this.optString(key)
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { deserializer(it) }
|
||||
?: default.also { put(key, serializer(it)) }
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
package io.bartek.ttsserver.core.web.dto
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
data class SonosDTO(val text: String, val language: Locale, val zone: String, val volume: Int) :
|
||||
DTO() {
|
||||
companion object {
|
||||
fun fromJSON(json: String) = JSONObject(json).let { root ->
|
||||
val language = root.nullableString("language") ?.let { Locale(it) } ?: Locale.US
|
||||
val text = root.requiredString("text")
|
||||
val zone = root.requiredString("zone")
|
||||
val volume = root.optInt("volume", 50)
|
||||
|
||||
SonosDTO(text, language, zone, volume)
|
||||
}
|
||||
}
|
||||
class SonosDTO(json: String) : DTO(json) {
|
||||
val language = nullableObject("language", Locale.US, { Locale(it) }, { it.toString() })
|
||||
val text = requiredString("text")
|
||||
val zone = requiredString("zone")
|
||||
val volume = nullableInt("volume", 50)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.bartek.ttsserver.core.web.exception
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.Response
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
class WebException(val status: Response.Status, message: String? = null) : Exception(message) {
|
||||
val json: String
|
||||
get() = message?.takeIf { it.isNotBlank() }
|
||||
?.let { JSONObject().put("message", it).toString() }
|
||||
?: ""
|
||||
}
|
||||
@@ -7,9 +7,6 @@ import android.net.Uri
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status.*
|
||||
import io.bartek.ttsserver.ui.preference.PreferenceKey
|
||||
import io.bartek.ttsserver.service.foreground.ForegroundService
|
||||
import io.bartek.ttsserver.service.state.ServiceState
|
||||
import io.bartek.ttsserver.core.sonos.queue.SonosQueue
|
||||
import io.bartek.ttsserver.core.tts.engine.TTSEngine
|
||||
import io.bartek.ttsserver.core.tts.status.TTSStatus
|
||||
@@ -17,6 +14,10 @@ import io.bartek.ttsserver.core.web.dto.BaseDTO
|
||||
import io.bartek.ttsserver.core.web.dto.SonosDTO
|
||||
import io.bartek.ttsserver.core.web.endpoint.Endpoint
|
||||
import io.bartek.ttsserver.core.web.endpoint.EndpointMatcher
|
||||
import io.bartek.ttsserver.core.web.exception.WebException
|
||||
import io.bartek.ttsserver.service.foreground.ForegroundService
|
||||
import io.bartek.ttsserver.service.state.ServiceState
|
||||
import io.bartek.ttsserver.ui.preference.PreferenceKey
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
@@ -37,10 +38,12 @@ class WebServer(
|
||||
return dispatch(it)
|
||||
}
|
||||
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
throw WebException(BAD_REQUEST, "Unknown error")
|
||||
} catch (e: WebException) {
|
||||
return newFixedLengthResponse(e.status, MIME_JSON, e.json)
|
||||
} catch (e: Exception) {
|
||||
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, e.toString())
|
||||
}
|
||||
catch (e: ResponseException) { throw e }
|
||||
catch (e: Exception) { throw ResponseException(INTERNAL_ERROR, e.toString(), e) }
|
||||
}
|
||||
|
||||
private fun dispatch(it: IHTTPSession): Response {
|
||||
@@ -49,37 +52,33 @@ class WebServer(
|
||||
Endpoint.WAVE -> wave(it)
|
||||
Endpoint.SONOS -> sonos(it)
|
||||
Endpoint.SONOS_CACHE -> sonosCache(it)
|
||||
Endpoint.UNKNOWN -> throw ResponseException(NOT_FOUND, "")
|
||||
Endpoint.UNKNOWN -> throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertThatTTSIsReady() {
|
||||
if (tts.status != TTSStatus.READY) {
|
||||
throw ResponseException(NOT_ACCEPTABLE, "Server is not ready yet")
|
||||
throw WebException(NOT_ACCEPTABLE, "Server is not ready yet")
|
||||
}
|
||||
}
|
||||
|
||||
private fun say(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
throw WebException(BAD_REQUEST, "The only accepted format is JSON")
|
||||
}
|
||||
|
||||
val (text, language) = extractBody(session) {
|
||||
BaseDTO.fromJSON(
|
||||
it
|
||||
)
|
||||
}
|
||||
val dto = extractBody(session) { BaseDTO(it) }
|
||||
|
||||
tts.performTTS(text, language)
|
||||
return newFixedLengthResponse(OK, MIME_PLAINTEXT, "")
|
||||
tts.performTTS(dto.text, dto.language)
|
||||
return newFixedLengthResponse(OK, MIME_JSON, dto.json)
|
||||
}
|
||||
|
||||
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
|
||||
@@ -91,73 +90,62 @@ class WebServer(
|
||||
|
||||
private fun wave(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_WAVE_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
throw WebException(BAD_REQUEST, "The only accepted format is JSON")
|
||||
}
|
||||
|
||||
val (text, language) = extractBody(session) {
|
||||
BaseDTO.fromJSON(
|
||||
it
|
||||
)
|
||||
}
|
||||
val dto = extractBody(session) {BaseDTO(it) }
|
||||
|
||||
val (stream, size) = tts.fetchTTSStream(text, language)
|
||||
return newFixedLengthResponse(OK,
|
||||
MIME_WAVE, stream, size)
|
||||
val (stream, size) = tts.fetchTTSStream(dto.text, dto.language)
|
||||
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
|
||||
}
|
||||
|
||||
private fun sonos(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw ResponseException(BAD_REQUEST, "")
|
||||
throw WebException(BAD_REQUEST, "The only accepted format is JSON")
|
||||
}
|
||||
|
||||
val data = extractBody(session) {
|
||||
SonosDTO.fromJSON(
|
||||
it
|
||||
)
|
||||
}
|
||||
val dto = extractBody(session) { SonosDTO(it) }
|
||||
|
||||
sonos.push(data)
|
||||
sonos.push(dto)
|
||||
|
||||
return newFixedLengthResponse(ACCEPTED, MIME_PLAINTEXT, "")
|
||||
return newFixedLengthResponse(ACCEPTED, MIME_JSON, dto.json)
|
||||
}
|
||||
|
||||
private fun sonosCache(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
if (session.method != Method.GET) {
|
||||
throw ResponseException(METHOD_NOT_ALLOWED, "")
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||
}
|
||||
|
||||
val filename =
|
||||
Uri.parse(session.uri).lastPathSegment ?: throw ResponseException(BAD_REQUEST, "")
|
||||
val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST, "")
|
||||
val file = File(context.cacheDir, filename)
|
||||
|
||||
if (!file.exists()) {
|
||||
throw ResponseException(NOT_FOUND, "")
|
||||
throw WebException(NOT_FOUND, "")
|
||||
}
|
||||
|
||||
val stream = BufferedInputStream(FileInputStream(file))
|
||||
val size = file.length()
|
||||
return newFixedLengthResponse(OK,
|
||||
MIME_WAVE, stream, size)
|
||||
return newFixedLengthResponse(OK, MIME_WAVE, stream, size)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
|
||||
Reference in New Issue
Block a user