Add support for file-based endpoints
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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"] ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user