Add support for file-based endpoints

This commit is contained in:
2020-07-01 22:42:08 +02:00
parent e4cdd29034
commit 549e0c162e
5 changed files with 127 additions and 102 deletions

View File

@@ -1,9 +1,11 @@
package com.bartlomiejpluta.ttsserver.core.lua.lib
import cafe.adriel.androidaudioconverter.model.AudioFormat
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import org.luaj.vm2.LuaNil
import org.luaj.vm2.LuaString
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.ThreeArgFunction
import org.luaj.vm2.lib.TwoArgFunction
import java.lang.IllegalArgumentException
import java.util.*
@@ -11,20 +13,36 @@ import java.util.*
class TTSLibrary(private val ttsEngine: TTSEngine) : TwoArgFunction() {
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
val tts = LuaValue.tableOf()
tts.set("performTTS", SayMethod(ttsEngine))
tts.set("say", SayMethod(ttsEngine))
tts.set("sayToFile", FileMethod(ttsEngine))
env.set("tts", tts)
return tts
val audioFormats = LuaValue.tableOf()
AudioFormat.values().forEach { audioFormats.set(it.name, it.name) }
env.set("AudioFormat", audioFormats)
return LuaValue.NIL
}
class SayMethod(private val ttsEngine: TTSEngine) : TwoArgFunction() {
override fun call(textArg: LuaValue, languageArg: LuaValue): LuaValue {
val text = textArg as? LuaString ?: throw IllegalArgumentException("Text should be a string")
val language = textArg as? LuaString ?: throw IllegalArgumentException("Language should be a string")
ttsEngine.performTTS(text.tojstring(), Locale.forLanguageTag(language.tojstring()))
override fun call(text: LuaValue, language: LuaValue): LuaValue {
ttsEngine.performTTS(text.checkjstring(), Locale.forLanguageTag(language.checkjstring()))
return LuaValue.NIL
}
}
class FileMethod(private val ttsEngine: TTSEngine) : ThreeArgFunction() {
override fun call(text: LuaValue, language: LuaValue, format: LuaValue): LuaValue {
val lang = Locale.forLanguageTag(language.checkjstring())
val audioFormat = format
.takeIf { it !is LuaNil }
?.let { AudioFormat.valueOf(it.checkjstring()) }
?: AudioFormat.WAV
val file = ttsEngine.createTTSFile(text.checkjstring(), lang, audioFormat)
return LuaValue.valueOf(file.absolutePath)
}
}
}

View File

@@ -2,7 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.lua.loader
import android.content.Context
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.Method
@@ -14,7 +14,7 @@ import java.lang.IllegalArgumentException
class EndpointLoader(private val context: Context, private val sandboxFactory: SandboxFactory) {
fun loadEndpoints(): List<Endpoint> {
fun loadEndpoints(): List<DefaultEndpoint> {
val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray()
return scripts
@@ -23,7 +23,7 @@ class EndpointLoader(private val context: Context, private val sandboxFactory: S
.map { createEndpoint(it) }
}
private fun createEndpoint(luaTable: LuaTable) = Endpoint(
private fun createEndpoint(luaTable: LuaTable) = DefaultEndpoint(
uri = parseUri(luaTable),
method = parseMethod(luaTable),
type = parseType(luaTable),

View File

@@ -0,0 +1,92 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.*
import org.luaj.vm2.LuaClosure
import org.luaj.vm2.LuaTable
import org.luaj.vm2.LuaValue
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
class DefaultEndpoint(
private val uri: UriTemplate,
private val type: EndpointType,
private val accepts: String,
private val method: Method,
private val consumer: LuaClosure
) : Endpoint {
override fun hit(session: IHTTPSession): Response? {
if (session.method != method) {
return null
}
val matchingResult = uri.match(session.uri)
if (!matchingResult.matched) {
return null
}
val params = LuaValue.tableOf().also { params ->
matchingResult.variables
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
.forEach { params.set(it.first, it.second) }
}
val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable()
return parseResponse(response)
}
private fun parseResponse(response: LuaValue) = response
.let {
it as? LuaTable
?: throw IllegalArgumentException("Invalid type for response - expected table")
}
.let { provideResponse(it) }
private fun provideResponse(response: LuaTable) =
when (response.get("type").checkjstring()) {
ResponseType.TEXT.name -> getTextResponse(response)
ResponseType.FILE.name -> getFileResponse(response)
else -> throw IllegalArgumentException("Unknown value for type in response")
}
private fun getTextResponse(response: LuaTable) = newFixedLengthResponse(
getStatus(response),
getMimeType(response),
getData(response)
)
private fun getFileResponse(response: LuaTable): Response? {
val file = File(response.get("file").checkstring().tojstring())
val stream = BufferedInputStream(FileInputStream(file))
val length = file.length()
return newFixedLengthResponse(
getStatus(response),
getMimeType(response),
stream,
length
)
}
private fun getStatus(response: LuaTable): Response.Status {
val status = response.get("status").checkint()
return Response.Status
.values()
.firstOrNull { it.requestStatus == status }
?: throw IllegalArgumentException("Unsupported status: $status")
}
private fun getMimeType(response: LuaTable) = response.get("mime").checkstring().tojstring()
private fun getData(response: LuaTable) = response.get("data").checkstring().tojstring()
private fun extractBody(session: IHTTPSession): String {
return mutableMapOf<String, String>().let {
session.parseBody(it)
it["postData"] ?: ""
}
}
}

View File

@@ -1,92 +1,9 @@
package com.bartlomiejpluta.ttsserver.core.web.endpoint
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
import fi.iki.elonen.NanoHTTPD.*
import org.luaj.vm2.LuaClosure
import org.luaj.vm2.LuaTable
import org.luaj.vm2.LuaValue
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.IHTTPSession
import fi.iki.elonen.NanoHTTPD.Response
class Endpoint(
private val uri: UriTemplate,
private val type: EndpointType,
private val accepts: String,
private val method: Method,
private val consumer: LuaClosure
) {
fun hit(session: IHTTPSession): Response? {
if (session.method != method) {
return null
}
val matchingResult = uri.match(session.uri)
if (!matchingResult.matched) {
return null
}
val params = LuaValue.tableOf().also { params ->
matchingResult.variables
.map { LuaValue.valueOf(it.key) to LuaValue.valueOf(it.value) }
.forEach { params.set(it.first, it.second) }
}
val response = consumer.call(LuaValue.valueOf(extractBody(session)), params).checktable()
return parseResponse(response)
}
private fun parseResponse(response: LuaValue) = response
.let {
it as? LuaTable
?: throw IllegalArgumentException("Invalid type for response - expected table")
}
.let { provideResponse(it) }
private fun provideResponse(response: LuaTable) =
when (response.get("type").checkjstring()) {
ResponseType.TEXT.name -> getTextResponse(response)
ResponseType.FILE.name -> getFileResponse(response)
else -> throw IllegalArgumentException("Unknown value for type in response")
}
private fun getTextResponse(response: LuaTable) = newFixedLengthResponse(
getStatus(response),
getMimeType(response),
getData(response)
)
private fun getFileResponse(response: LuaTable): Response? {
val file = File(response.get("file").checkstring().tojstring())
val stream = BufferedInputStream(FileInputStream(file))
val length = file.length()
return newFixedLengthResponse(
getStatus(response),
getMimeType(response),
stream,
length
)
}
private fun getStatus(response: LuaTable): Response.Status {
val status = response.get("status").checkint()
return Response.Status
.values()
.firstOrNull { it.requestStatus == status }
?: throw IllegalArgumentException("Unsupported status: $status")
}
private fun getMimeType(response: LuaTable) = response.get("mime").checkstring().tojstring()
private fun getData(response: LuaTable) = response.get("data").checkstring().tojstring()
private fun extractBody(session: IHTTPSession): String {
return mutableMapOf<String, String>().let {
session.parseBody(it)
it["postData"] ?: ""
}
}
interface Endpoint {
fun hit(session: IHTTPSession): Response?
}

View File

@@ -4,12 +4,10 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
import com.bartlomiejpluta.ttsserver.core.web.endpoint.DefaultEndpoint
import com.bartlomiejpluta.ttsserver.core.web.exception.WebException
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
@@ -26,7 +24,7 @@ class WebServer(
private val preferences: SharedPreferences,
private val tts: TTSEngine,
private val sonos: SonosQueue,
private val endpoints: List<Endpoint>
private val endpoints: List<DefaultEndpoint>
) : NanoHTTPD(port) {
private val speakersSilenceSchedulerEnabled: Boolean
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)