Add kotlin-console example using kotlinx.cli and the library

This commit is contained in:
Niels van Velzen 2020-07-20 21:48:52 +02:00
parent 4faeaed126
commit d4925d43b0
10 changed files with 243 additions and 0 deletions

View File

@ -0,0 +1,17 @@
# Kotlin Console Example
This sample project uses the kotlinx-cli library to build a command line tool that uses the Jellyfin
library. It's used as a showcase of the libraries abilities and is not meant for general use.
Features include:
- Server discovery
- Authenticate
- List libraries
## Basic usage
Assuming the binary is called `jellyfin` the following sample will list all libraries in the demo instance:
```sh
jellyfin libraries --server https://demo.jellyfin.org/stable --token $(jellyfin login --server https://demo.jellyfin.org/stable --username demo)
```
This command is also provided in the `test.sh` file. It requires a local install first using `./gradlew installDist`.

View File

@ -0,0 +1,36 @@
plugins {
kotlin("jvm")
id("application")
}
application {
mainClassName = "org.jellyfin.sample.console.MainKt"
}
repositories {
jcenter()
// Repository needed for kotlinx-cli
maven("https://kotlin.bintray.com/kotlinx")
}
dependencies {
// Depend on the library project
implementation(project(":library"))
// Use Kotlin stdlib
implementation(kotlin("stdlib"))
// Use Kotlin coroutines to interact with the library
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5")
// The CLI library
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.2.1")
// Use JSON
implementation("com.google.code.gson:gson:2.8.6")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlinx.cli.ExperimentalCli"
}

View File

@ -0,0 +1,33 @@
package org.jellyfin.sample.console
import kotlinx.cli.ArgParser
import org.jellyfin.apiclient.AppInfo
import org.jellyfin.apiclient.Jellyfin
import org.jellyfin.apiclient.interaction.device.IDevice
import org.jellyfin.sample.console.cli.Discover
import org.jellyfin.sample.console.cli.Libraries
import org.jellyfin.sample.console.cli.Login
import org.jellyfin.sample.console.utils.GarbageHttpClient
fun main(args: Array<String>) {
val jellyfin = Jellyfin {
appInfo = AppInfo("Jellyfin Sample: Kotlin Console", "DEV")
httpClient = GarbageHttpClient()
}
val device = object : IDevice {
override val deviceName: String = "cli"
override val deviceId: String = "cli"
}
ArgParser("jellyfin").apply {
subcommands(Discover(jellyfin))
subcommands(Login(jellyfin, device))
subcommands(Libraries(jellyfin, device))
parse(args)
// parse(arrayOf("discover"))
// parse("login --server https://demo.jellyfin.org/stable --username demo --password ".split(" ").toTypedArray())
// parse("libraries --server https://demo.jellyfin.org/stable --token 1d1e113ab39e4804bb42580b4323810b".split(" ").toTypedArray())
}
}

View File

@ -0,0 +1,20 @@
package org.jellyfin.sample.console.cli
import kotlinx.cli.Subcommand
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.jellyfin.apiclient.Jellyfin
class Discover(
private val jellyfin: Jellyfin
) : Subcommand("discover", "Discover servers on the local network") {
override fun execute() = runBlocking {
println("Starting discovery")
jellyfin.discovery.discover().onEach {
println("Server ${it.name} was found at address ${it.address}:")
println(" $it")
}.collect()
}
}

View File

@ -0,0 +1,37 @@
package org.jellyfin.sample.console.cli
import kotlinx.cli.ArgType
import kotlinx.cli.Subcommand
import kotlinx.cli.required
import kotlinx.coroutines.runBlocking
import org.jellyfin.apiclient.Jellyfin
import org.jellyfin.apiclient.interaction.device.IDevice
import org.jellyfin.apiclient.model.querying.ItemsResult
import org.jellyfin.apiclient.model.session.SessionInfoDto
import org.jellyfin.sample.console.utils.callApi
class Libraries(
private val jellyfin: Jellyfin,
private val device: IDevice
) : Subcommand("libraries", "List all libraries") {
val server by option(ArgType.String, description = "Url of the server", shortName = "s").required()
val token by option(ArgType.String, description = "Access token", shortName = "t").required()
override fun execute() = runBlocking {
val api = jellyfin.createApi(serverAddress = server, accessToken = token, device = device)
val sessionInfo = callApi<Array<SessionInfoDto>> { callback ->
api.GetCurrentSessionAsync(callback)
}.firstOrNull()
if (sessionInfo == null) println("Unknown session")
val libraries = callApi<ItemsResult> { callback ->
api.GetUserViews(sessionInfo!!.userId, callback)
}
libraries.items.forEach {
println(it.name)
}
}
}

View File

@ -0,0 +1,29 @@
package org.jellyfin.sample.console.cli
import kotlinx.cli.ArgType
import kotlinx.cli.Subcommand
import kotlinx.cli.required
import kotlinx.coroutines.runBlocking
import org.jellyfin.apiclient.Jellyfin
import org.jellyfin.apiclient.interaction.device.IDevice
import org.jellyfin.apiclient.model.users.AuthenticationResult
import org.jellyfin.sample.console.utils.callApi
class Login(
private val jellyfin: Jellyfin,
private val device: IDevice
) : Subcommand("login", "Login to a given server and retrieve an access token") {
val server by option(ArgType.String, description = "Url of the server", shortName = "s").required()
val username by option(ArgType.String, description = "Username", shortName = "u").required()
val password by option(ArgType.String, description = "Password", shortName = "p")
override fun execute() = runBlocking {
val api = jellyfin.createApi(serverAddress = server, device = device)
val result = callApi<AuthenticationResult> { callback ->
api.AuthenticateUserAsync(username, password ?: "", callback)
}
if (result.accessToken != null) println(result.accessToken)
}
}

View File

@ -0,0 +1,48 @@
package org.jellyfin.sample.console.utils
import org.jellyfin.apiclient.interaction.Response
import org.jellyfin.apiclient.interaction.http.HttpRequest
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient
import java.net.HttpURLConnection
import java.net.URL
class GarbageHttpClient : IAsyncHttpClient {
override fun Send(request: HttpRequest?, response: Response<String>?) {
requireNotNull(request)
requireNotNull(response)
val headers = request.requestHeaders.toMap().toMutableMap()
// Magic
if (request.requestContentType != null && !headers.containsKey("Content-Type"))
headers["Content-Type"] = request.requestContentType
else if (!request.postData.isNullOrEmpty() && !headers.containsKey("Content-Type"))
headers["Content-Type"] = "application/x-www-form-urlencoded"
if (request.requestHeaders.authorizationParameter != null)
headers["X-Emby-Authorization"] = "${request.requestHeaders.authorizationScheme} ${request.requestHeaders.authorizationParameter}"
try {
val connection = URL(request.url).openConnection() as HttpURLConnection
connection.apply {
useCaches = request.enableCaching
requestMethod = request.method
headers.forEach { header ->
setRequestProperty(header.key, header.value)
}
if (!request.postData.isNullOrEmpty()) {
doOutput = true
outputStream.write(request.postData.GetQueryString().toByteArray())
}
}
val res = connection.inputStream.readBytes().toString(Charsets.UTF_8)
response.onResponse(res)
} catch (err: Exception) {
response.onError(err)
}
}
}

View File

@ -0,0 +1,11 @@
package org.jellyfin.sample.console.utils
import org.jellyfin.apiclient.interaction.Response
import kotlin.coroutines.suspendCoroutine
suspend fun <T : Any> callApi(init: (callback: Response<T>) -> Unit): T = suspendCoroutine { continuation ->
init(object : Response<T>() {
override fun onResponse(response: T) = continuation.resumeWith(Result.success(response))
override fun onError(exception: Exception) = continuation.resumeWith(Result.failure(exception))
})
}

View File

@ -0,0 +1,9 @@
#!/bin/bash
function jellyfin() {
"$(pwd)/build/install/kotlin-console/bin/kotlin-console" "$@"
}
server=https://demo.jellyfin.org/stable
jellyfin libraries --server $server --token "$(jellyfin login --server $server --username demo)"

View File

@ -4,3 +4,6 @@ include(":model")
// Platforms
include(":android")
// Samples
include(":samples:kotlin-console")