Create working PoC
This commit is contained in:
@@ -38,6 +38,7 @@ dependencies {
|
||||
implementation 'com.google.dagger:dagger-android:2.15'
|
||||
implementation 'com.google.dagger:dagger-android-support:2.15'
|
||||
implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8'
|
||||
implementation 'org.luaj:luaj-jse:3.0.1'
|
||||
kapt 'com.google.dagger:dagger-android-processor:2.15'
|
||||
kapt 'com.google.dagger:dagger-compiler:2.15'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name="com.bartlomiejpluta.ttsserver.TTSApplication"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.lua.lib
|
||||
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointType
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import org.luaj.vm2.LuaValue
|
||||
import org.luaj.vm2.lib.TwoArgFunction
|
||||
|
||||
class HTTPLibrary : TwoArgFunction() {
|
||||
override fun call(modname: LuaValue, env: LuaValue): LuaValue {
|
||||
val methods = LuaValue.tableOf()
|
||||
val responses = LuaValue.tableOf()
|
||||
val endpoints = LuaValue.tableOf()
|
||||
NanoHTTPD.Method.values().forEach { methods.set(it.name, it.name) }
|
||||
NanoHTTPD.Response.Status.values().forEach { responses.set(it.name, it.requestStatus) }
|
||||
EndpointType.values().forEach { endpoints.set(it.name, it.name) }
|
||||
env.set("Method", methods)
|
||||
env.set("Response", responses)
|
||||
env.set("Endpoint", endpoints)
|
||||
return methods
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.lua.lib
|
||||
|
||||
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.TwoArgFunction
|
||||
import java.lang.IllegalArgumentException
|
||||
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))
|
||||
env.set("tts", tts)
|
||||
return tts
|
||||
}
|
||||
|
||||
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()))
|
||||
|
||||
return LuaValue.NIL
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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.EndpointType
|
||||
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
|
||||
import fi.iki.elonen.NanoHTTPD.Method
|
||||
import org.luaj.vm2.LuaClosure
|
||||
import org.luaj.vm2.LuaNil
|
||||
import org.luaj.vm2.LuaString
|
||||
import org.luaj.vm2.LuaTable
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class EndpointLoader(private val context: Context, private val sandboxFactory: SandboxFactory) {
|
||||
|
||||
fun loadEndpoints(): List<Endpoint> {
|
||||
val scripts = context.getExternalFilesDir("Endpoints")?.listFiles() ?: emptyArray()
|
||||
|
||||
return scripts
|
||||
.map { sandboxFactory.createSandbox().loadfile(it.absolutePath).call() }
|
||||
.map { it as? LuaTable ?: throw IllegalArgumentException("Expected single table to be returned") }
|
||||
.map { createEndpoint(it) }
|
||||
}
|
||||
|
||||
private fun createEndpoint(luaTable: LuaTable) = Endpoint(
|
||||
uri = parseUri(luaTable),
|
||||
method = parseMethod(luaTable),
|
||||
type = parseType(luaTable),
|
||||
accepts = parseAccepts(luaTable),
|
||||
consumer = parseConsumer(luaTable)
|
||||
)
|
||||
|
||||
private fun parseUri(luaTable: LuaTable) = luaTable.get("uri")
|
||||
.takeIf { it !is LuaNil }
|
||||
?.let { it as? LuaString ?: throw IllegalArgumentException("'uri' must be of string type'") }
|
||||
?.tojstring()
|
||||
?.let { UriTemplate.parse(it) }
|
||||
?: throw IllegalArgumentException("'uri' field is required")
|
||||
|
||||
private fun parseConsumer(luaTable: LuaTable) = luaTable.get("consumer")
|
||||
.takeIf { it !is LuaNil }
|
||||
?.let { it as? LuaClosure ?: throw IllegalArgumentException("'consumer' must be a function'") }
|
||||
?: throw IllegalArgumentException("'consumer' field is required")
|
||||
|
||||
private fun parseAccepts(luaTable: LuaTable) = luaTable.get("accepts")
|
||||
.takeIf { it !is LuaNil }
|
||||
?.let { it as? LuaString ?: throw IllegalArgumentException("'accepts' must be of string type'") }
|
||||
?.tojstring()
|
||||
?: "text/plain"
|
||||
|
||||
private fun parseType(luaTable: LuaTable) = luaTable.get("type")
|
||||
.takeIf { it !is LuaNil }
|
||||
?.let { it as? LuaString ?: throw IllegalArgumentException("'type' must be of string type'") }
|
||||
?.let { EndpointType.valueOf(it.tojstring()) }
|
||||
?: EndpointType.DEFAULT
|
||||
|
||||
private fun parseMethod(luaTable: LuaTable) = luaTable.get("method")
|
||||
.takeIf { it !is LuaNil }
|
||||
?.let { it as? LuaString ?: throw IllegalArgumentException("'method' must be of string type'") }
|
||||
?.let { Method.valueOf(it.tojstring()) }
|
||||
?: Method.GET
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.lua.sandbox
|
||||
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary
|
||||
import org.luaj.vm2.Globals
|
||||
import org.luaj.vm2.LoadState
|
||||
import org.luaj.vm2.compiler.LuaC
|
||||
import org.luaj.vm2.lib.PackageLib
|
||||
import org.luaj.vm2.lib.StringLib
|
||||
import org.luaj.vm2.lib.TableLib
|
||||
import org.luaj.vm2.lib.jse.JseBaseLib
|
||||
import org.luaj.vm2.lib.jse.JseMathLib
|
||||
import org.luaj.vm2.lib.jse.JseOsLib
|
||||
|
||||
class SandboxFactory(
|
||||
private val httpLibrary: HTTPLibrary,
|
||||
private val ttsLibrary: TTSLibrary
|
||||
) {
|
||||
fun createSandbox() = Globals().also {
|
||||
it.load(JseBaseLib())
|
||||
it.load(PackageLib())
|
||||
it.load(TableLib())
|
||||
it.load(StringLib())
|
||||
it.load(JseMathLib())
|
||||
it.load(JseOsLib())
|
||||
it.load(httpLibrary)
|
||||
it.load(ttsLibrary)
|
||||
LoadState.install(it)
|
||||
LuaC.install(it)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import android.content.SharedPreferences
|
||||
import cafe.adriel.androidaudioconverter.model.AudioFormat
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||
import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpointx
|
||||
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||
@@ -20,8 +20,8 @@ class SonosWorker(
|
||||
private val preferences: SharedPreferences,
|
||||
private val queue: BlockingQueue<SonosDTO>
|
||||
) : Runnable {
|
||||
private val gongUrl: String get() = address + Endpoint.GONG.trimmedUri
|
||||
private val announcementUrl: String get() = address + Endpoint.SONOS_CACHE.trimmedUri
|
||||
private val gongUrl: String get() = address + Endpointx.GONG.trimmedUri
|
||||
private val announcementUrl: String get() = address + Endpointx.SONOS_CACHE.trimmedUri
|
||||
private var snapshot: Snapshot? = null
|
||||
|
||||
override fun run() = try {
|
||||
|
||||
@@ -1,22 +1,92 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||
|
||||
enum class Endpoint(val uri: String, val id: Int) {
|
||||
UNKNOWN("/", 1),
|
||||
SAY("/say", 2),
|
||||
WAVE("/wave", 3),
|
||||
AAC("/aac", 4),
|
||||
MP3("/mp3", 5),
|
||||
M4A("/m4a", 6),
|
||||
WMA("/wma", 7),
|
||||
FLAC("/flac", 8),
|
||||
SONOS("/sonos", 9),
|
||||
SONOS_CACHE("/sonos/*", 10),
|
||||
GONG("/gong.wav", 11);
|
||||
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
|
||||
|
||||
val trimmedUri: String
|
||||
get() = uri.replace("*", "")
|
||||
class Endpoint(
|
||||
private val uri: UriTemplate,
|
||||
private val type: EndpointType,
|
||||
private val accepts: String,
|
||||
private val method: Method,
|
||||
private val consumer: LuaClosure
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
|
||||
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"] ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ object EndpointMatcher {
|
||||
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
|
||||
|
||||
init {
|
||||
Endpoint.values().forEach {
|
||||
Endpointx.values().forEach {
|
||||
uriMatcher.addURI("", it.uri, it.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
fun match(uri: String) =
|
||||
Endpoint.of(uriMatcher.match(Uri.parse("content://$uri")))
|
||||
Endpointx.of(uriMatcher.match(Uri.parse("content://$uri")))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||
|
||||
enum class EndpointType {
|
||||
DEFAULT,
|
||||
QUEUE
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||
|
||||
enum class Endpointx(val uri: String, val id: Int) {
|
||||
UNKNOWN("/", 1),
|
||||
SAY("/say", 2),
|
||||
WAVE("/wave", 3),
|
||||
AAC("/aac", 4),
|
||||
MP3("/mp3", 5),
|
||||
M4A("/m4a", 6),
|
||||
WMA("/wma", 7),
|
||||
FLAC("/flac", 8),
|
||||
SONOS("/sonos", 9),
|
||||
SONOS_CACHE("/sonos/*", 10),
|
||||
GONG("/gong.wav", 11);
|
||||
|
||||
val trimmedUri: String
|
||||
get() = uri.replace("*", "")
|
||||
|
||||
companion object {
|
||||
fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||
|
||||
enum class ResponseType {
|
||||
TEXT,
|
||||
FILE
|
||||
}
|
||||
@@ -3,19 +3,14 @@ package com.bartlomiejpluta.ttsserver.core.web.server
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import cafe.adriel.androidaudioconverter.model.AudioFormat
|
||||
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.exception.TTSException
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatus
|
||||
import com.bartlomiejpluta.ttsserver.core.web.dto.BaseDTO
|
||||
import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.Endpoint
|
||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointMatcher
|
||||
import com.bartlomiejpluta.ttsserver.core.web.exception.WebException
|
||||
import com.bartlomiejpluta.ttsserver.core.web.mime.MimeType
|
||||
import com.bartlomiejpluta.ttsserver.service.foreground.ForegroundService
|
||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||
@@ -23,10 +18,6 @@ import com.bartlomiejpluta.ttsserver.ui.preference.model.TimeRange
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status.*
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
|
||||
|
||||
class WebServer(
|
||||
@@ -34,7 +25,8 @@ class WebServer(
|
||||
private val context: Context,
|
||||
private val preferences: SharedPreferences,
|
||||
private val tts: TTSEngine,
|
||||
private val sonos: SonosQueue
|
||||
private val sonos: SonosQueue,
|
||||
private val endpoints: List<Endpoint>
|
||||
) : NanoHTTPD(port) {
|
||||
private val speakersSilenceSchedulerEnabled: Boolean
|
||||
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
|
||||
@@ -72,20 +64,12 @@ class WebServer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatch(it: IHTTPSession): Response {
|
||||
return when (EndpointMatcher.match(it.uri)) {
|
||||
Endpoint.SAY -> say(it)
|
||||
Endpoint.WAVE -> file(it, AudioFormat.WAV)
|
||||
Endpoint.AAC -> file(it, AudioFormat.AAC)
|
||||
Endpoint.MP3 -> file(it, AudioFormat.MP3)
|
||||
Endpoint.M4A -> file(it, AudioFormat.M4A)
|
||||
Endpoint.WMA -> file(it, AudioFormat.WMA)
|
||||
Endpoint.FLAC -> file(it, AudioFormat.FLAC)
|
||||
Endpoint.SONOS -> sonos(it)
|
||||
Endpoint.SONOS_CACHE -> sonosCache(it)
|
||||
Endpoint.GONG -> gong(it)
|
||||
Endpoint.UNKNOWN -> throw WebException(NOT_FOUND)
|
||||
private fun dispatch(session: IHTTPSession): Response {
|
||||
for (endpoint in endpoints) {
|
||||
endpoint.hit(session)?.let { return it }
|
||||
}
|
||||
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
private fun handleWebException(e: WebException) =
|
||||
@@ -100,28 +84,28 @@ class WebServer(
|
||||
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, stacktrace)
|
||||
}
|
||||
|
||||
private fun say(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
}
|
||||
|
||||
if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||
}
|
||||
|
||||
val dto = extractBody(session) { BaseDTO(it) }
|
||||
|
||||
tts.performTTS(dto.text, dto.language)
|
||||
return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
|
||||
}
|
||||
// private fun say(session: IHTTPSession): Response {
|
||||
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// if (session.method != Method.POST) {
|
||||
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
// }
|
||||
//
|
||||
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
// }
|
||||
//
|
||||
// if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||
// }
|
||||
//
|
||||
// val dto = extractBody(session) { BaseDTO(it) }
|
||||
//
|
||||
// tts.performTTS(dto.text, dto.language)
|
||||
// return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
|
||||
// }
|
||||
|
||||
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
|
||||
return mutableMapOf<String, String>().let {
|
||||
@@ -130,92 +114,92 @@ class WebServer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
}
|
||||
|
||||
val dto = extractBody(session) { BaseDTO(it) }
|
||||
|
||||
val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
|
||||
return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
|
||||
}
|
||||
|
||||
private fun sonos(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
if (session.method != Method.POST) {
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
}
|
||||
|
||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
}
|
||||
|
||||
if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||
}
|
||||
|
||||
val dto = extractBody(session) { SonosDTO(it) }
|
||||
|
||||
sonos.push(dto)
|
||||
|
||||
return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
|
||||
}
|
||||
|
||||
private fun sonosCache(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
if (session.method != Method.GET) {
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||
}
|
||||
|
||||
val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
|
||||
val file = File(context.cacheDir, filename)
|
||||
|
||||
if (!file.exists()) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
val stream = BufferedInputStream(FileInputStream(file))
|
||||
val size = file.length()
|
||||
return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
|
||||
}
|
||||
|
||||
private fun gong(session: IHTTPSession): Response {
|
||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
|
||||
throw WebException(NOT_FOUND)
|
||||
}
|
||||
|
||||
if (session.method != Method.GET) {
|
||||
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||
}
|
||||
|
||||
val uri = Uri.parse(
|
||||
preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
|
||||
)
|
||||
|
||||
val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
|
||||
?: throw TTSException()
|
||||
|
||||
val stream = BufferedInputStream(
|
||||
context.contentResolver.openInputStream(uri) ?: throw TTSException()
|
||||
)
|
||||
|
||||
return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
|
||||
}
|
||||
// private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
|
||||
// if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// if (session.method != Method.POST) {
|
||||
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
// }
|
||||
//
|
||||
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
// }
|
||||
//
|
||||
// val dto = extractBody(session) { BaseDTO(it) }
|
||||
//
|
||||
// val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
|
||||
// return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
|
||||
// }
|
||||
//
|
||||
// private fun sonos(session: IHTTPSession): Response {
|
||||
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// if (session.method != Method.POST) {
|
||||
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||
// }
|
||||
//
|
||||
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||
// }
|
||||
//
|
||||
// if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||
// }
|
||||
//
|
||||
// val dto = extractBody(session) { SonosDTO(it) }
|
||||
//
|
||||
// sonos.push(dto)
|
||||
//
|
||||
// return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
|
||||
// }
|
||||
//
|
||||
// private fun sonosCache(session: IHTTPSession): Response {
|
||||
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// if (session.method != Method.GET) {
|
||||
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||
// }
|
||||
//
|
||||
// val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
|
||||
// val file = File(context.cacheDir, filename)
|
||||
//
|
||||
// if (!file.exists()) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// val stream = BufferedInputStream(FileInputStream(file))
|
||||
// val size = file.length()
|
||||
// return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
|
||||
// }
|
||||
//
|
||||
// private fun gong(session: IHTTPSession): Response {
|
||||
// if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
|
||||
// throw WebException(NOT_FOUND)
|
||||
// }
|
||||
//
|
||||
// if (session.method != Method.GET) {
|
||||
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||
// }
|
||||
//
|
||||
// val uri = Uri.parse(
|
||||
// preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
|
||||
// )
|
||||
//
|
||||
// val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
|
||||
// ?: throw TTSException()
|
||||
//
|
||||
// val stream = BufferedInputStream(
|
||||
// context.contentResolver.openInputStream(uri) ?: throw TTSException()
|
||||
// )
|
||||
//
|
||||
// return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
|
||||
// }
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.web.server
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
|
||||
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||
@@ -10,13 +11,15 @@ class WebServerFactory(
|
||||
private val preferences: SharedPreferences,
|
||||
private val context: Context,
|
||||
private val tts: TTSEngine,
|
||||
private val sonos: SonosQueue
|
||||
private val sonos: SonosQueue,
|
||||
private val endpointLoader: EndpointLoader
|
||||
) {
|
||||
fun createWebServer() =
|
||||
WebServer(
|
||||
preferences.getInt(
|
||||
PreferenceKey.PORT,
|
||||
8080
|
||||
), context, preferences, tts, sonos
|
||||
)
|
||||
fun createWebServer() = WebServer(
|
||||
preferences.getInt(PreferenceKey.PORT, 8080),
|
||||
context,
|
||||
preferences,
|
||||
tts,
|
||||
sonos,
|
||||
endpointLoader.loadEndpoints()
|
||||
)
|
||||
}
|
||||
@@ -28,10 +28,9 @@ class UriTemplate private constructor(uri: String) {
|
||||
variables.add(variableBuilder.toString())
|
||||
}
|
||||
|
||||
isVariable -> char.takeIf { it.isLetter() } ?: error(
|
||||
"Only letters are allowed as template",
|
||||
index + 1
|
||||
).let { variableBuilder?.append(it) }
|
||||
isVariable -> char.takeIf { it.isLetter() }
|
||||
?.let { variableBuilder?.append(it) }
|
||||
?: error("Only letters are allowed as template", index + 1)
|
||||
|
||||
else -> patternBuilder.append(char)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bartlomiejpluta.ttsserver.di.component
|
||||
import android.content.Context
|
||||
import com.bartlomiejpluta.ttsserver.TTSApplication
|
||||
import com.bartlomiejpluta.ttsserver.di.module.AndroidModule
|
||||
import com.bartlomiejpluta.ttsserver.di.module.LuaModule
|
||||
import com.bartlomiejpluta.ttsserver.di.module.TTSModule
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
@@ -11,7 +12,7 @@ import dagger.android.support.AndroidSupportInjectionModule
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class])
|
||||
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class, LuaModule::class])
|
||||
interface AppComponent : AndroidInjector<TTSApplication> {
|
||||
|
||||
@Component.Builder
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.bartlomiejpluta.ttsserver.di.module
|
||||
|
||||
import android.content.Context
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.lib.HTTPLibrary
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.lib.TTSLibrary
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.sandbox.SandboxFactory
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class LuaModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun endpointLoader(context: Context, sandboxFactory: SandboxFactory) =
|
||||
EndpointLoader(context, sandboxFactory)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun sandboxFactory(httpLibrary: HTTPLibrary, ttsLibrary: TTSLibrary) =
|
||||
SandboxFactory(httpLibrary, ttsLibrary)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun httpLibrary() = HTTPLibrary()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun ttsLibrary(ttsEngine: TTSEngine) = TTSLibrary(ttsEngine)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.speech.tts.TextToSpeech
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.bartlomiejpluta.ttsserver.core.lua.loader.EndpointLoader
|
||||
import com.bartlomiejpluta.ttsserver.core.sonos.queue.SonosQueue
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
|
||||
@@ -43,12 +44,14 @@ class TTSModule {
|
||||
preferences: SharedPreferences,
|
||||
context: Context,
|
||||
tts: TTSEngine,
|
||||
sonos: SonosQueue
|
||||
sonos: SonosQueue,
|
||||
endpointLoader: EndpointLoader
|
||||
) = WebServerFactory(
|
||||
preferences,
|
||||
context,
|
||||
tts,
|
||||
sonos
|
||||
sonos,
|
||||
endpointLoader
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
Reference in New Issue
Block a user