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:2.15'
|
||||||
implementation 'com.google.dagger:dagger-android-support:2.15'
|
implementation 'com.google.dagger:dagger-android-support:2.15'
|
||||||
implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8'
|
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-android-processor:2.15'
|
||||||
kapt 'com.google.dagger:dagger-compiler:2.15'
|
kapt 'com.google.dagger:dagger-compiler:2.15'
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
|
<uses-permission android:name="com.bartlomiejpluta.permission.TTS_HTTP_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="com.bartlomiejpluta.ttsserver.TTSApplication"
|
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 cafe.adriel.androidaudioconverter.model.AudioFormat
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||||
import com.bartlomiejpluta.ttsserver.core.web.dto.SonosDTO
|
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.foreground.ForegroundService
|
||||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||||
@@ -20,8 +20,8 @@ class SonosWorker(
|
|||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val queue: BlockingQueue<SonosDTO>
|
private val queue: BlockingQueue<SonosDTO>
|
||||||
) : Runnable {
|
) : Runnable {
|
||||||
private val gongUrl: String get() = address + Endpoint.GONG.trimmedUri
|
private val gongUrl: String get() = address + Endpointx.GONG.trimmedUri
|
||||||
private val announcementUrl: String get() = address + Endpoint.SONOS_CACHE.trimmedUri
|
private val announcementUrl: String get() = address + Endpointx.SONOS_CACHE.trimmedUri
|
||||||
private var snapshot: Snapshot? = null
|
private var snapshot: Snapshot? = null
|
||||||
|
|
||||||
override fun run() = try {
|
override fun run() = try {
|
||||||
|
|||||||
@@ -1,22 +1,92 @@
|
|||||||
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
package com.bartlomiejpluta.ttsserver.core.web.endpoint
|
||||||
|
|
||||||
enum class Endpoint(val uri: String, val id: Int) {
|
import com.bartlomiejpluta.ttsserver.core.web.uri.UriTemplate
|
||||||
UNKNOWN("/", 1),
|
import fi.iki.elonen.NanoHTTPD.*
|
||||||
SAY("/say", 2),
|
import org.luaj.vm2.LuaClosure
|
||||||
WAVE("/wave", 3),
|
import org.luaj.vm2.LuaTable
|
||||||
AAC("/aac", 4),
|
import org.luaj.vm2.LuaValue
|
||||||
MP3("/mp3", 5),
|
import java.io.BufferedInputStream
|
||||||
M4A("/m4a", 6),
|
import java.io.File
|
||||||
WMA("/wma", 7),
|
import java.io.FileInputStream
|
||||||
FLAC("/flac", 8),
|
|
||||||
SONOS("/sonos", 9),
|
|
||||||
SONOS_CACHE("/sonos/*", 10),
|
|
||||||
GONG("/gong.wav", 11);
|
|
||||||
|
|
||||||
val trimmedUri: String
|
class Endpoint(
|
||||||
get() = uri.replace("*", "")
|
private val uri: UriTemplate,
|
||||||
|
private val type: EndpointType,
|
||||||
|
private val accepts: String,
|
||||||
|
private val method: Method,
|
||||||
|
private val consumer: LuaClosure
|
||||||
|
) {
|
||||||
|
|
||||||
companion object {
|
fun hit(session: IHTTPSession): Response? {
|
||||||
fun of(ordinal: Int) = values().firstOrNull { it.ordinal == ordinal } ?: UNKNOWN
|
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)
|
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Endpoint.values().forEach {
|
Endpointx.values().forEach {
|
||||||
uriMatcher.addURI("", it.uri, it.ordinal)
|
uriMatcher.addURI("", it.uri, it.ordinal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun match(uri: String) =
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
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.sonos.queue.SonosQueue
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
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.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.Endpoint
|
||||||
import com.bartlomiejpluta.ttsserver.core.web.endpoint.EndpointMatcher
|
|
||||||
import com.bartlomiejpluta.ttsserver.core.web.exception.WebException
|
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.foreground.ForegroundService
|
||||||
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
import com.bartlomiejpluta.ttsserver.service.state.ServiceState
|
||||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
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
|
||||||
import fi.iki.elonen.NanoHTTPD.Response.Status.*
|
import fi.iki.elonen.NanoHTTPD.Response.Status.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
class WebServer(
|
class WebServer(
|
||||||
@@ -34,7 +25,8 @@ class WebServer(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val tts: TTSEngine,
|
private val tts: TTSEngine,
|
||||||
private val sonos: SonosQueue
|
private val sonos: SonosQueue,
|
||||||
|
private val endpoints: List<Endpoint>
|
||||||
) : NanoHTTPD(port) {
|
) : NanoHTTPD(port) {
|
||||||
private val speakersSilenceSchedulerEnabled: Boolean
|
private val speakersSilenceSchedulerEnabled: Boolean
|
||||||
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
|
get() = preferences.getBoolean(PreferenceKey.ENABLE_SPEAKERS_SILENCE_SCHEDULER, false)
|
||||||
@@ -72,20 +64,12 @@ class WebServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatch(it: IHTTPSession): Response {
|
private fun dispatch(session: IHTTPSession): Response {
|
||||||
return when (EndpointMatcher.match(it.uri)) {
|
for (endpoint in endpoints) {
|
||||||
Endpoint.SAY -> say(it)
|
endpoint.hit(session)?.let { return 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw WebException(NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWebException(e: WebException) =
|
private fun handleWebException(e: WebException) =
|
||||||
@@ -100,28 +84,28 @@ class WebServer(
|
|||||||
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, stacktrace)
|
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, stacktrace)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun say(session: IHTTPSession): Response {
|
// private fun say(session: IHTTPSession): Response {
|
||||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SAY_ENDPOINT, true)) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.method != Method.POST) {
|
// if (session.method != Method.POST) {
|
||||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
|
// if (speakersSilenceSchedulerEnabled && speakersSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||||
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val dto = extractBody(session) { BaseDTO(it) }
|
// val dto = extractBody(session) { BaseDTO(it) }
|
||||||
|
//
|
||||||
tts.performTTS(dto.text, dto.language)
|
// tts.performTTS(dto.text, dto.language)
|
||||||
return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
|
// return newFixedLengthResponse(OK, MIME_JSON, SUCCESS_RESPONSE)
|
||||||
}
|
// }
|
||||||
|
|
||||||
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
|
private fun <T> extractBody(session: IHTTPSession, provider: (String) -> T): T {
|
||||||
return mutableMapOf<String, String>().let {
|
return mutableMapOf<String, String>().let {
|
||||||
@@ -130,92 +114,92 @@ class WebServer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
|
// private fun file(session: IHTTPSession, audioFormat: AudioFormat): Response {
|
||||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
|
// if (!preferences.getBoolean(PreferenceKey.ENABLE_FILE_ENDPOINTS, true)) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.method != Method.POST) {
|
// if (session.method != Method.POST) {
|
||||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val dto = extractBody(session) { BaseDTO(it) }
|
// val dto = extractBody(session) { BaseDTO(it) }
|
||||||
|
//
|
||||||
val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
|
// val (stream, size) = tts.fetchTTSStream(dto.text, dto.language, audioFormat)
|
||||||
return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
|
// return newFixedLengthResponse(OK, MimeType.forAudioFormat(audioFormat).mimeType, stream, size)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun sonos(session: IHTTPSession): Response {
|
// private fun sonos(session: IHTTPSession): Response {
|
||||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.method != Method.POST) {
|
// if (session.method != Method.POST) {
|
||||||
throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
// throw WebException(METHOD_NOT_ALLOWED, "Only POST methods are allowed")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
// if (session.headers[CONTENT_TYPE]?.let { it != MIME_JSON } != false) {
|
||||||
throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
// throw WebException(BAD_REQUEST, "Only JSON data is accepted")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
|
// if (sonosSilenceSchedulerEnabled && sonosSilenceSchedule.inRange(Calendar.getInstance())) {
|
||||||
return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
// return newFixedLengthResponse(NO_CONTENT, MIME_JSON, "")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val dto = extractBody(session) { SonosDTO(it) }
|
// val dto = extractBody(session) { SonosDTO(it) }
|
||||||
|
//
|
||||||
sonos.push(dto)
|
// sonos.push(dto)
|
||||||
|
//
|
||||||
return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
|
// return newFixedLengthResponse(ACCEPTED, MIME_JSON, QUEUED_RESPONSE)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun sonosCache(session: IHTTPSession): Response {
|
// private fun sonosCache(session: IHTTPSession): Response {
|
||||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
// if (!preferences.getBoolean(PreferenceKey.ENABLE_SONOS_ENDPOINT, true)) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.method != Method.GET) {
|
// if (session.method != Method.GET) {
|
||||||
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
|
// val filename = Uri.parse(session.uri).lastPathSegment ?: throw WebException(BAD_REQUEST)
|
||||||
val file = File(context.cacheDir, filename)
|
// val file = File(context.cacheDir, filename)
|
||||||
|
//
|
||||||
if (!file.exists()) {
|
// if (!file.exists()) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val stream = BufferedInputStream(FileInputStream(file))
|
// val stream = BufferedInputStream(FileInputStream(file))
|
||||||
val size = file.length()
|
// val size = file.length()
|
||||||
return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
|
// return newFixedLengthResponse(OK, MimeType.forFile(file).mimeType, stream, size)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private fun gong(session: IHTTPSession): Response {
|
// private fun gong(session: IHTTPSession): Response {
|
||||||
if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
|
// if (!preferences.getBoolean(PreferenceKey.ENABLE_GONG, false)) {
|
||||||
throw WebException(NOT_FOUND)
|
// throw WebException(NOT_FOUND)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (session.method != Method.GET) {
|
// if (session.method != Method.GET) {
|
||||||
throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
// throw WebException(METHOD_NOT_ALLOWED, "Only GET methods are allowed")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
val uri = Uri.parse(
|
// val uri = Uri.parse(
|
||||||
preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
|
// preferences.getString(PreferenceKey.GONG, null) ?: throw TTSException()
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
|
// val size = context.contentResolver.openFileDescriptor(uri, "r")?.statSize
|
||||||
?: throw TTSException()
|
// ?: throw TTSException()
|
||||||
|
//
|
||||||
val stream = BufferedInputStream(
|
// val stream = BufferedInputStream(
|
||||||
context.contentResolver.openInputStream(uri) ?: throw TTSException()
|
// context.contentResolver.openInputStream(uri) ?: throw TTSException()
|
||||||
)
|
// )
|
||||||
|
//
|
||||||
return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
|
// return newFixedLengthResponse(OK, MimeType.WAV.mimeType, stream, size)
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
super.start()
|
super.start()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bartlomiejpluta.ttsserver.core.web.server
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
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.sonos.queue.SonosQueue
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||||
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
import com.bartlomiejpluta.ttsserver.ui.preference.key.PreferenceKey
|
||||||
@@ -10,13 +11,15 @@ class WebServerFactory(
|
|||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val tts: TTSEngine,
|
private val tts: TTSEngine,
|
||||||
private val sonos: SonosQueue
|
private val sonos: SonosQueue,
|
||||||
|
private val endpointLoader: EndpointLoader
|
||||||
) {
|
) {
|
||||||
fun createWebServer() =
|
fun createWebServer() = WebServer(
|
||||||
WebServer(
|
preferences.getInt(PreferenceKey.PORT, 8080),
|
||||||
preferences.getInt(
|
context,
|
||||||
PreferenceKey.PORT,
|
preferences,
|
||||||
8080
|
tts,
|
||||||
), context, preferences, tts, sonos
|
sonos,
|
||||||
)
|
endpointLoader.loadEndpoints()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,9 @@ class UriTemplate private constructor(uri: String) {
|
|||||||
variables.add(variableBuilder.toString())
|
variables.add(variableBuilder.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
isVariable -> char.takeIf { it.isLetter() } ?: error(
|
isVariable -> char.takeIf { it.isLetter() }
|
||||||
"Only letters are allowed as template",
|
?.let { variableBuilder?.append(it) }
|
||||||
index + 1
|
?: error("Only letters are allowed as template", index + 1)
|
||||||
).let { variableBuilder?.append(it) }
|
|
||||||
|
|
||||||
else -> patternBuilder.append(char)
|
else -> patternBuilder.append(char)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bartlomiejpluta.ttsserver.di.component
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bartlomiejpluta.ttsserver.TTSApplication
|
import com.bartlomiejpluta.ttsserver.TTSApplication
|
||||||
import com.bartlomiejpluta.ttsserver.di.module.AndroidModule
|
import com.bartlomiejpluta.ttsserver.di.module.AndroidModule
|
||||||
|
import com.bartlomiejpluta.ttsserver.di.module.LuaModule
|
||||||
import com.bartlomiejpluta.ttsserver.di.module.TTSModule
|
import com.bartlomiejpluta.ttsserver.di.module.TTSModule
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
@@ -11,7 +12,7 @@ import dagger.android.support.AndroidSupportInjectionModule
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class])
|
@Component(modules = [AndroidSupportInjectionModule::class, AndroidModule::class, TTSModule::class, LuaModule::class])
|
||||||
interface AppComponent : AndroidInjector<TTSApplication> {
|
interface AppComponent : AndroidInjector<TTSApplication> {
|
||||||
|
|
||||||
@Component.Builder
|
@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.content.SharedPreferences
|
||||||
import android.speech.tts.TextToSpeech
|
import android.speech.tts.TextToSpeech
|
||||||
import androidx.preference.PreferenceManager
|
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.sonos.queue.SonosQueue
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
import com.bartlomiejpluta.ttsserver.core.tts.engine.TTSEngine
|
||||||
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
|
import com.bartlomiejpluta.ttsserver.core.tts.status.TTSStatusHolder
|
||||||
@@ -43,12 +44,14 @@ class TTSModule {
|
|||||||
preferences: SharedPreferences,
|
preferences: SharedPreferences,
|
||||||
context: Context,
|
context: Context,
|
||||||
tts: TTSEngine,
|
tts: TTSEngine,
|
||||||
sonos: SonosQueue
|
sonos: SonosQueue,
|
||||||
|
endpointLoader: EndpointLoader
|
||||||
) = WebServerFactory(
|
) = WebServerFactory(
|
||||||
preferences,
|
preferences,
|
||||||
context,
|
context,
|
||||||
tts,
|
tts,
|
||||||
sonos
|
sonos,
|
||||||
|
endpointLoader
|
||||||
)
|
)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
Reference in New Issue
Block a user