Merge pull request #69 from nielsvanvelzen/jellyfin-entry

Add Jellyfin class which should be used when using the library
This commit is contained in:
Bill Thornton 2020-07-20 12:47:58 -04:00 committed by GitHub
commit 4faeaed126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 235 additions and 152 deletions

208
README.md
View File

@ -44,178 +44,82 @@
This library allows Java and Android applications to easily access the Jellyfin API. It is built with Volley, OkHttp, Boon, and Robolectric. The dependencies are modular and can easily be swapped out with alternate implementations when desired.
## Single Server Example
## Android Example
This is an example of connecting to a single server using a fixed address from an app that has requires a user login.
This is an example of connecting to a single server using a fixed address from an Android app that has requires a user login.
``` java
// Developers should create their own logger implementation
logger = new NullLogger();
```kotlin
// Create a Jellyfin instance
val jellyfin = Jellyfin {
// It is recommended to create an own logger implementation
logger = NullLogger()
android(context)
}
// The underlying http stack. Developers can inject their own if desired
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, getApplicationContext());
// Create a new api client
val apiClient = jellyfin.createApi(
serverAddress = "http://localhost:8096",
device = AndroidDevice.fromContext(context)
)
// Android developers should use AndroidDevice
IDevice device = new Device("deviceId", "deviceName");
// Call authenticate function
apiClient.AuthenticateUserAsync("username", "password", object : Response<AuthenticationResult>() {
override fun onResponse(result: AuthenticationResult) {
// Authentication succeeded
}
ApiClient apiClient = new ApiClient(httpClient, logger, "http://localhost:8096", "My app name", "app version 123", device, new ApiEventListener());
apiClient.AuthenticateUserAsync("username", "password", new Response<AuthenticationResult>() {
@Override
public void onResponse(AuthenticationResult result) {
// Authentication succeeded
}
@Override
public void onError() {
// Authentication failed
}
});
```
## Service Apps
If your app is some kind of service or utility (e.g. Sickbeard), you should construct ApiClient with an API key supplied by your users.
``` java
// Developers should create their own logger implementation
logger = new NullLogger();
// The underlying http stack. Developers can inject their own if desired
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, getApplicationContext());
// Services should just authenticate using their api key
ApiClient apiClient = new ApiClient(httpClient, logger, "http://localhost:8096", "apikey", new ApiEventListener());
override fun onError(error: Exception) {
// Authentication failed
}
})
```
## Web Socket
Once you have an ApiClient instance you can easily connect to the server's web socket using the following command.
``` java
ApiClient.OpenWebSocket();
```kotlin
apiClient.OpenWebSocket()
```
This will open a connection in a background thread, and periodically check to ensure it's still connected. The web socket provides various events that can be used to receive notifications from the server. Simply override the methods in ApiEventListener:
This will open a connection in a background thread, and periodically check to ensure it's still connected. The web socket provides various events that can be used to receive notifications from the server. Simply override the methods in the ApiEventListener class which can be passed to the "createApi" function.
``` java
@Override
public void onSetVolumeCommand(int value) {
```kotlin
override fun onSetVolumeCommand(value: Int) {
}
```
## Multi-Server Usage
## Using Java
The above examples are designed for cases when your app always connects to a single server, and you always know the address. If your app is designed to support multiple networks and/or multiple servers, then **IConnectionManager** should be used in place of the above example.
The Jellyfin library supports both Java and Kotlin out of the box. The basic Android example in Java looks like this
IConnectionManager features:
```java
// Create the options using the options builder
JellyfinOptions.Builder options = new JellyfinOptions.Builder();
options.setLogger(new NullLogger());
JellyfinAndroidKt.android(options, context);
- Supports connection to multiple servers
- Automatic local server discovery
- Wake on Lan
- Automatic LAN to WAN failover
// Create a Jellyfin instance
Jellyfin jellyfin = new Jellyfin(options.build());
``` java
// Android developer should use AndroidCredentialProvider
ICredentialProvider credentialProvider = new CredentialProvider();
INetworkConnection networkConnection = new NetworkConnection(logger);
// Developers are encouraged to create their own ILogger implementation
ILogger logger = new NullLogger();
// The underlying http stack. Developers can inject their own if desired
IAsyncHttpClient httpClient = new VolleyHttpClient(logger, getApplicationContext());
// Android developers should use AndroidDevice
IDevice device = new Device("deviceId", "deviceName");
// This describes the device capabilities
ClientCapabilities capabilities = new ClientCapabilities();
ApiEventListener eventListener = new ApiEventListener();
// Android developers should use AndroidConnectionManager
IConnectionManager connectionManager = new ConnectionManager(credentialProvider,
networkConnection,
logger,
httpClient,
"My app name"
"1.0.0.0",
device,
capabilities,
eventListener);
```
## Multi-Server Startup Workflow
After you've created your instance of IConnectionManager, simply call the Connect method. It will return a result object with three properties:
- State
- ServerInfo
- ApiClient
ServerInfo and ApiClient will be null if State == Unavailable. Let's look at an example.
``` java
connectionManager.Connect(new Response<ConnectionResult>() {
@Override
public void onResponse(ConnectionResult result) {
switch (result.getState()) {
case ConnectionState.ConnectSignIn:
// Connect sign in screen should be presented
// Authenticate using LoginToConnect, then call Connect again to start over
case ConnectionState.ServerSignIn:
// A server was found and the user needs to login.
// Display a login screen and authenticate with the server using result.ApiClient
case ConnectionState.ServerSelection:
// Multiple servers available
// Display a selection screen by calling GetAvailableServers
// When a server is chosen, call the Connect overload that accept either a ServerInfo object or a String url.
case ConnectionState.SignedIn:
// A server was found and the user has been signed in using previously saved credentials.
// Ready to browse using result.ApiClient
}
}
// Create a new api client
ApiClient apiClient = jellyfin.createApi(
"http://localhost:8096",
null,
AndroidDevice.fromContext(context),
new ApiEventListener()
);
// Call authenticate function
apiClient.AuthenticateUserAsync("username", "password", new Response<AuthenticationResult>() {
@Override
public void onResponse(AuthenticationResult response) {
// Authentication succeeded
}
@Override
public void onError(Exception exception) {
// Authentication failed
}
});
```
When the user wishes to logout of the individual server simply call apiClient.Logout() with no special parameters. If the user will to connect to a new server use the Connect overload which accepts an address for the new server.
``` java
String address = "http://192.168.1.174:8096";
connectionManager.Connect(address, new Response<ConnectionResult>() {
@Override
public void onResponse(ConnectionResult result) {
switch (result.State) {
case ConnectionState.Unavailable:
// Server unreachable
case ConnectionState.ServerSignIn:
// A server was found and the user needs to login.
// Display a login screen and authenticate with the server using result.ApiClient
case ConnectionState.SignedIn:
// A server was found and the user has been signed in using previously saved credentials.
// Ready to browse using result.ApiClient
}
}
);
```
If at anytime the RemoteLoggedOut event is fired, simply start the workflow all over again by calling connectionManager.Connect().
ConnectionManager will handle opening and closing web socket connections at the appropiate times. All your app needs to do is use an ApiClient instance to subscribe to individual events.
``` java
@Override
public void onSetVolumeCommand(int value) {
}
```
With multi-server connectivity it is not recommended to keep a global ApiClient instance, or pass an ApiClient around the application. Instead keep a factory that will resolve the appropriate ApiClient instance depending on context. In order to help with this, ConnectionManager has a GetApiClient method that accepts a BaseItemDto and returns an ApiClient from the server it belongs to.
## Android Usage
Android is fully supported, and special subclasses are provided for it:
- AndroidConnectionManager
- AndroidApiClient

View File

@ -0,0 +1,14 @@
package org.jellyfin.apiclient
import android.content.Context
import org.jellyfin.apiclient.discovery.AndroidBroadcastAddressesProvider
import org.jellyfin.apiclient.interaction.VolleyHttpClient
/**
* Add default Android configuration.
* Only run after setting the logger.
*/
fun JellyfinOptions.Builder.android(context: Context) {
discoveryBroadcastAddressesProvider = AndroidBroadcastAddressesProvider(context)
httpClient = VolleyHttpClient(logger, context)
}

View File

@ -29,6 +29,7 @@ data class AndroidDevice(
else "$manufacturer $model"
}
@JvmStatic
fun fromContext(context: Context) = AndroidDevice(
deviceId = getAutomaticId(context),
deviceName = getAutomaticName(context)

View File

@ -0,0 +1,6 @@
package org.jellyfin.apiclient
data class AppInfo(
val name: String,
val version: String
)

View File

@ -0,0 +1,43 @@
package org.jellyfin.apiclient
import com.google.gson.Gson
import org.jellyfin.apiclient.discovery.IDiscoveryBroadcastAddressesProvider
import org.jellyfin.apiclient.discovery.ServerDiscovery
import org.jellyfin.apiclient.logging.ILogger
import java.util.regex.Pattern
class DiscoveryService(
private val gson: Gson,
private val logger: ILogger,
private val discoveryBroadcastAddressesProvider: IDiscoveryBroadcastAddressesProvider
) {
private val serverDiscovery by lazy {
// Create instance
ServerDiscovery(gson, logger, discoveryBroadcastAddressesProvider)
}
// Address helper
fun addressCandidates(input: String): Iterable<String> {
val candidates = mutableListOf<String>()
// Check for protocol
val containsProtocol = Pattern.compile("^https?://.*$", Pattern.CASE_INSENSITIVE).matcher(input).matches()
if (!containsProtocol) {
candidates.add("https://$input")
candidates.add("http://$input")
} else {
candidates.add(input)
}
return candidates
}
// Discovery
fun discover(
timeout: Int = ServerDiscovery.DISCOVERY_TIMEOUT,
maxServers: Int = ServerDiscovery.DISCOVERY_MAX_SERVERS
) = serverDiscovery.discover(
timeout,
maxServers
)
}

View File

@ -0,0 +1,37 @@
package org.jellyfin.apiclient
import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder
import org.jellyfin.apiclient.interaction.ApiClient
import org.jellyfin.apiclient.interaction.ApiEventListener
import org.jellyfin.apiclient.interaction.device.IDevice
class Jellyfin(
private val options: JellyfinOptions
) {
constructor(initOptions: JellyfinOptions.Builder.() -> Unit) : this(JellyfinOptions.build(initOptions))
private val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
.create()
val discovery by lazy {
DiscoveryService(gson, options.logger, options.discoverBroadcastAddressesProvider)
}
fun createApi(
serverAddress: String,
accessToken: String? = null,
device: IDevice? = null,
eventListener: ApiEventListener = ApiEventListener()
) = ApiClient(
options.httpClient,
options.logger,
serverAddress,
accessToken,
options.appInfo.name,
options.appInfo.version,
device,
eventListener
)
}

View File

@ -0,0 +1,36 @@
package org.jellyfin.apiclient
import org.jellyfin.apiclient.discovery.IDiscoveryBroadcastAddressesProvider
import org.jellyfin.apiclient.discovery.JavaNetBroadcastAddressesProvider
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient
import org.jellyfin.apiclient.logging.ILogger
import org.jellyfin.apiclient.logging.NullLogger
data class JellyfinOptions(
val logger: ILogger,
val discoverBroadcastAddressesProvider: IDiscoveryBroadcastAddressesProvider,
val appInfo: AppInfo,
val httpClient: IAsyncHttpClient
) {
class Builder {
var logger: ILogger = NullLogger()
var discoveryBroadcastAddressesProvider: IDiscoveryBroadcastAddressesProvider = JavaNetBroadcastAddressesProvider()
var appInfo: AppInfo = AppInfo(name = "Unknown", version = "?")
var httpClient: IAsyncHttpClient = NoHttpClient()
fun build() = JellyfinOptions(
logger,
discoveryBroadcastAddressesProvider,
appInfo,
httpClient
)
}
companion object {
fun build(init: Builder.() -> Unit): JellyfinOptions {
val builder = Builder()
builder.init()
return builder.build()
}
}
}

View File

@ -0,0 +1,11 @@
package org.jellyfin.apiclient
import org.jellyfin.apiclient.interaction.Response
import org.jellyfin.apiclient.interaction.http.HttpRequest
import org.jellyfin.apiclient.interaction.http.IAsyncHttpClient
class NoHttpClient : IAsyncHttpClient {
override fun Send(request: HttpRequest?, response: Response<String>?) {
throw NotImplementedError("Making HTTP requests is not possible because no http client was passed to the Jellyfin options constructor.")
}
}

View File

@ -134,6 +134,16 @@ public class ApiClient extends BaseApiClient {
ResetHttpHeaders();
}
public ApiClient(IAsyncHttpClient httpClient, ILogger logger, String serverAddress, String accessToken, String appName, String applicationVersion, IDevice device, ApiEventListener apiEventListener)
{
super(logger, serverAddress, accessToken, appName, device, applicationVersion);
this.httpClient = httpClient;
this.apiEventListener = apiEventListener;
ResetHttpHeaders();
}
public void EnableAutomaticNetworking(ServerInfo info)
{
this.serverInfo = info;

View File

@ -106,6 +106,27 @@ public abstract class BaseApiClient
setAccessToken(accessToken);
setServerAddress(serverAddress);
}
protected BaseApiClient(ILogger logger, String serverAddress, String accessToken, String clientName, IDevice device, String applicationVersion)
{
if (logger == null)
{
throw new IllegalArgumentException("logger");
}
if (tangible.DotNetToJavaStringHelper.isNullOrEmpty(serverAddress))
{
throw new IllegalArgumentException("serverAddress");
}
setJsonSerializer(new GsonJsonSerializer());
Logger = logger;
setClientName(clientName);
this.device = device;
setApplicationVersion(applicationVersion);
setAccessToken(accessToken);
setServerAddress(serverAddress);
}
/**
Gets the name of the server host.