diff --git a/jellyfin-api-ktor/api/jellyfin-api-ktor.api b/jellyfin-api-ktor/api/jellyfin-api-ktor.api index b298d2081..49ac53306 100644 --- a/jellyfin-api-ktor/api/jellyfin-api-ktor.api +++ b/jellyfin-api-ktor/api/jellyfin-api-ktor.api @@ -7,7 +7,7 @@ public class org/jellyfin/sdk/api/ktor/KtorClient : org/jellyfin/sdk/api/client/ public fun getDeviceInfo ()Lorg/jellyfin/sdk/model/DeviceInfo; public fun getHttpClientOptions ()Lorg/jellyfin/sdk/api/client/HttpClientOptions; public fun getWebSocket ()Lorg/jellyfin/sdk/api/sockets/SocketApi; - public fun request (Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun request (Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/ranges/IntRange;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun setAccessToken (Ljava/lang/String;)V public fun setBaseUrl (Ljava/lang/String;)V public fun setClientInfo (Lorg/jellyfin/sdk/model/ClientInfo;)V diff --git a/jellyfin-api-ktor/src/commonMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt b/jellyfin-api-ktor/src/commonMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt index a4db518e4..f5bc9fb0a 100644 --- a/jellyfin-api-ktor/src/commonMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt +++ b/jellyfin-api-ktor/src/commonMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt @@ -23,5 +23,6 @@ public expect open class KtorClient( pathParameters: Map, queryParameters: Map, requestBody: Any?, + expectedResponse: IntRange, ): RawResponse } diff --git a/jellyfin-api-ktor/src/jvmMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt b/jellyfin-api-ktor/src/jvmMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt index 79ff121ca..fc72100bc 100644 --- a/jellyfin-api-ktor/src/jvmMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt +++ b/jellyfin-api-ktor/src/jvmMain/kotlin/org/jellyfin/sdk/api/ktor/KtorClient.kt @@ -14,7 +14,6 @@ import io.ktor.content.ByteArrayContent import io.ktor.content.TextContent import io.ktor.http.ContentType import io.ktor.http.HttpHeaders -import io.ktor.http.isSuccess import io.ktor.util.toMap import kotlinx.serialization.SerializationException import mu.KotlinLogging @@ -84,6 +83,7 @@ public actual open class KtorClient actual constructor( pathParameters: Map, queryParameters: Map, requestBody: Any?, + expectedResponse: IntRange, ): RawResponse { val url = createUrl(pathTemplate, pathParameters, queryParameters) @@ -129,7 +129,7 @@ public actual open class KtorClient actual constructor( } // Check HTTP status - if (!response.status.isSuccess()) throw InvalidStatusException(response.status.value) + if (response.status.value !in expectedResponse) throw InvalidStatusException(response.status.value) // Return custom response instance return RawResponse(response.bodyAsChannel(), response.status.value, response.headers.toMap()) } catch (err: UnknownHostException) { @@ -168,5 +168,6 @@ public actual open class KtorClient actual constructor( HttpMethod.GET -> KtorHttpMethod.Get HttpMethod.POST -> KtorHttpMethod.Post HttpMethod.DELETE -> KtorHttpMethod.Delete + HttpMethod.HEAD -> KtorHttpMethod.Head } } diff --git a/jellyfin-api/api/jellyfin-api.api b/jellyfin-api/api/jellyfin-api.api index c6cfeca12..a136d5170 100644 --- a/jellyfin-api/api/jellyfin-api.api +++ b/jellyfin-api/api/jellyfin-api.api @@ -12,8 +12,8 @@ public abstract class org/jellyfin/sdk/api/client/ApiClient { public abstract fun getHttpClientOptions ()Lorg/jellyfin/sdk/api/client/HttpClientOptions; public final fun getOrCreateApi (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Lorg/jellyfin/sdk/api/operations/Api; public abstract fun getWebSocket ()Lorg/jellyfin/sdk/api/sockets/SocketApi; - public abstract fun request (Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun request$default (Lorg/jellyfin/sdk/api/client/ApiClient;Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun request (Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/ranges/IntRange;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun request$default (Lorg/jellyfin/sdk/api/client/ApiClient;Lorg/jellyfin/sdk/api/client/HttpMethod;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/lang/Object;Lkotlin/ranges/IntRange;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public abstract fun setAccessToken (Ljava/lang/String;)V public abstract fun setBaseUrl (Ljava/lang/String;)V public abstract fun setClientInfo (Lorg/jellyfin/sdk/model/ClientInfo;)V @@ -48,6 +48,7 @@ public final class org/jellyfin/sdk/api/client/HttpClientOptions { public final class org/jellyfin/sdk/api/client/HttpMethod : java/lang/Enum { public static final field DELETE Lorg/jellyfin/sdk/api/client/HttpMethod; public static final field GET Lorg/jellyfin/sdk/api/client/HttpMethod; + public static final field HEAD Lorg/jellyfin/sdk/api/client/HttpMethod; public static final field POST Lorg/jellyfin/sdk/api/client/HttpMethod; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lorg/jellyfin/sdk/api/client/HttpMethod; diff --git a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/ApiClient.kt b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/ApiClient.kt index 4701f353c..1b8620e69 100644 --- a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/ApiClient.kt +++ b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/ApiClient.kt @@ -91,6 +91,7 @@ public abstract class ApiClient { pathParameters: Map = emptyMap(), queryParameters: Map = emptyMap(), requestBody: Any? = null, + expectedResponse: IntRange = 200 until 300, ): RawResponse /** diff --git a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/HttpMethod.kt b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/HttpMethod.kt index 8a9504868..acc8c1a4a 100644 --- a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/HttpMethod.kt +++ b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/HttpMethod.kt @@ -4,4 +4,5 @@ public enum class HttpMethod { GET, POST, DELETE, + HEAD, } diff --git a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/extensions/HttpMethodExtensions.kt b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/extensions/HttpMethodExtensions.kt index 8183d1902..a50dc537b 100644 --- a/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/extensions/HttpMethodExtensions.kt +++ b/jellyfin-api/src/commonMain/kotlin/org/jellyfin/sdk/api/client/extensions/HttpMethodExtensions.kt @@ -42,3 +42,17 @@ public suspend inline fun ApiClient.delete( queryParameters = queryParameters, requestBody = requestBody ).createResponse() + +public suspend inline fun ApiClient.head( + pathTemplate: String, + pathParameters: Map = emptyMap(), + queryParameters: Map = emptyMap(), + requestBody: Any? = null, +): Response = request( + method = HttpMethod.HEAD, + pathTemplate = pathTemplate, + pathParameters = pathParameters, + queryParameters = queryParameters, + requestBody = requestBody, + expectedResponse = 300 until 400 +).createResponse() diff --git a/jellyfin-core/api/android/jellyfin-core.api b/jellyfin-core/api/android/jellyfin-core.api index bbcdca648..f7d72bb0f 100644 --- a/jellyfin-core/api/android/jellyfin-core.api +++ b/jellyfin-core/api/android/jellyfin-core.api @@ -130,9 +130,9 @@ public final class org/jellyfin/sdk/discovery/DiscoveryService { public static synthetic fun discoverLocalServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;IIILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public final fun getAddressCandidates (Ljava/lang/String;)Ljava/util/Collection; public final fun getRecommendedServers (Ljava/lang/String;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getRecommendedServers (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getRecommendedServers (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/lang/String;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class org/jellyfin/sdk/discovery/LocalServerDiscovery { @@ -152,26 +152,31 @@ public final class org/jellyfin/sdk/discovery/LocalServerDiscovery$Companion { public final class org/jellyfin/sdk/discovery/RecommendedServerDiscovery { public fun (Lorg/jellyfin/sdk/Jellyfin;)V - public final fun discover (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun discover (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun discover$default (Lorg/jellyfin/sdk/discovery/RecommendedServerDiscovery;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class org/jellyfin/sdk/discovery/RecommendedServerInfo { - public fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;)V + public fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()J public final fun component3 ()Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore; public final fun component4 ()Ljava/util/Collection; public final fun component5-d1pmJ48 ()Ljava/lang/Object; - public final fun copy (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; - public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerInfo;Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Lkotlin/Result;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; + public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerInfo;Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Lkotlin/Result;Ljava/lang/String;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; public fun equals (Ljava/lang/Object;)Z public final fun firstIssueOrNull ()Lorg/jellyfin/sdk/discovery/RecommendedServerIssue; public final fun getAddress ()Ljava/lang/String; public final fun getIssues ()Ljava/util/Collection; + public final fun getOriginalAddress ()Ljava/lang/String; public final fun getResponseTime ()J public final fun getScore ()Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore; public final fun getSystemInfo-d1pmJ48 ()Ljava/lang/Object; public fun hashCode ()I + public final fun isRedirect ()Z public fun toString ()Ljava/lang/String; } @@ -228,6 +233,19 @@ public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$OutdatedSer public fun toString ()Ljava/lang/String; } +public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse : org/jellyfin/sdk/discovery/RecommendedServerIssue { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse; + public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getFinalAddress ()Ljava/lang/String; + public final fun getOriginalAddress ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$SecureConnectionFailed : org/jellyfin/sdk/discovery/RecommendedServerIssue { public fun (Lorg/jellyfin/sdk/api/client/exception/SecureConnectionException;)V public final fun component1 ()Lorg/jellyfin/sdk/api/client/exception/SecureConnectionException; diff --git a/jellyfin-core/api/jvm/jellyfin-core.api b/jellyfin-core/api/jvm/jellyfin-core.api index 1eb860956..04028889c 100644 --- a/jellyfin-core/api/jvm/jellyfin-core.api +++ b/jellyfin-core/api/jvm/jellyfin-core.api @@ -122,9 +122,9 @@ public final class org/jellyfin/sdk/discovery/DiscoveryService { public static synthetic fun discoverLocalServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;IIILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public final fun getAddressCandidates (Ljava/lang/String;)Ljava/util/Collection; public final fun getRecommendedServers (Ljava/lang/String;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public final fun getRecommendedServers (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getRecommendedServers (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/lang/String;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static synthetic fun getRecommendedServers$default (Lorg/jellyfin/sdk/discovery/DiscoveryService;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class org/jellyfin/sdk/discovery/LocalServerDiscovery { @@ -144,26 +144,31 @@ public final class org/jellyfin/sdk/discovery/LocalServerDiscovery$Companion { public final class org/jellyfin/sdk/discovery/RecommendedServerDiscovery { public fun (Lorg/jellyfin/sdk/Jellyfin;)V - public final fun discover (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun discover (Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun discover$default (Lorg/jellyfin/sdk/discovery/RecommendedServerDiscovery;Ljava/util/Collection;Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; } public final class org/jellyfin/sdk/discovery/RecommendedServerInfo { - public fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;)V + public fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()J public final fun component3 ()Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore; public final fun component4 ()Ljava/util/Collection; public final fun component5-d1pmJ48 ()Ljava/lang/Object; - public final fun copy (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; - public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerInfo;Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Lkotlin/Result;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Ljava/lang/Object;Ljava/lang/String;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; + public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerInfo;Ljava/lang/String;JLorg/jellyfin/sdk/discovery/RecommendedServerInfoScore;Ljava/util/Collection;Lkotlin/Result;Ljava/lang/String;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerInfo; public fun equals (Ljava/lang/Object;)Z public final fun firstIssueOrNull ()Lorg/jellyfin/sdk/discovery/RecommendedServerIssue; public final fun getAddress ()Ljava/lang/String; public final fun getIssues ()Ljava/util/Collection; + public final fun getOriginalAddress ()Ljava/lang/String; public final fun getResponseTime ()J public final fun getScore ()Lorg/jellyfin/sdk/discovery/RecommendedServerInfoScore; public final fun getSystemInfo-d1pmJ48 ()Ljava/lang/Object; public fun hashCode ()I + public final fun isRedirect ()Z public fun toString ()Ljava/lang/String; } @@ -220,6 +225,19 @@ public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$OutdatedSer public fun toString ()Ljava/lang/String; } +public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse : org/jellyfin/sdk/discovery/RecommendedServerIssue { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse; + public static synthetic fun copy$default (Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lorg/jellyfin/sdk/discovery/RecommendedServerIssue$RedirectedResponse; + public fun equals (Ljava/lang/Object;)Z + public final fun getFinalAddress ()Ljava/lang/String; + public final fun getOriginalAddress ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class org/jellyfin/sdk/discovery/RecommendedServerIssue$SecureConnectionFailed : org/jellyfin/sdk/discovery/RecommendedServerIssue { public fun (Lorg/jellyfin/sdk/api/client/exception/SecureConnectionException;)V public final fun component1 ()Lorg/jellyfin/sdk/api/client/exception/SecureConnectionException; diff --git a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/DiscoveryService.kt b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/DiscoveryService.kt index 4b7a023ce..6c109a356 100644 --- a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/DiscoveryService.kt +++ b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/DiscoveryService.kt @@ -45,9 +45,11 @@ public class DiscoveryService( public suspend fun getRecommendedServers( servers: Collection, minimumScore: RecommendedServerInfoScore = RecommendedServerInfoScore.BAD, + followRedirects: Boolean = false, ): Collection = recommendedServerDiscovery.discover( servers = servers, - minimumScore = minimumScore + minimumScore = minimumScore, + followRedirects = followRedirects ) /** diff --git a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerDiscovery.kt b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerDiscovery.kt index 85876271a..4f7df2b45 100644 --- a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerDiscovery.kt +++ b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerDiscovery.kt @@ -1,5 +1,8 @@ package org.jellyfin.sdk.discovery +import io.ktor.http.HttpHeaders +import io.ktor.http.URLBuilder +import io.ktor.http.isRelativePath import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -15,7 +18,9 @@ import org.jellyfin.sdk.api.client.exception.InvalidContentException import org.jellyfin.sdk.api.client.exception.InvalidStatusException import org.jellyfin.sdk.api.client.exception.SecureConnectionException import org.jellyfin.sdk.api.client.exception.TimeoutException +import org.jellyfin.sdk.api.client.extensions.head import org.jellyfin.sdk.api.client.extensions.systemApi +import org.jellyfin.sdk.api.client.util.UrlBuilder.buildUrl import org.jellyfin.sdk.model.ServerVersion import org.jellyfin.sdk.model.api.PublicSystemInfo import org.jellyfin.sdk.util.currentTimeMillis @@ -35,11 +40,19 @@ public class RecommendedServerDiscovery constructor( } private data class SystemInfoResult( - val address: String, + val address: RedirectInfo, val systemInfo: Result, val responseTime: Long, ) + private data class RedirectInfo( + val originalAddress: String, + val redirectAddress: String? = null, + ) { + fun isRedirect() = redirectAddress != null && originalAddress != redirectAddress + fun getAddress() = redirectAddress ?: originalAddress + } + @Suppress("MagicNumber") private fun assignScore(result: SystemInfoResult): RecommendedServerInfo { val systemInfo = result.systemInfo.getOrNull() @@ -101,23 +114,30 @@ public class RecommendedServerDiscovery constructor( } } + // prefer non-redirected addresses + if (result.address.isRedirect()) { + issues.add(RecommendedServerIssue.RedirectedResponse(result.address.originalAddress, result.address.getAddress())) + scores.add(RecommendedServerInfoScore.GOOD) + } + // Calculate score, pick the lowest from the collection or use GREAT when no scores (and issues) added val score = scores.minByOrNull { it.score } ?: RecommendedServerInfoScore.GREAT // Return results return RecommendedServerInfo( - result.address, + result.address.getAddress(), result.responseTime, score, issues, result.systemInfo, + if (result.address.isRedirect()) result.address.originalAddress else null, ) } - private suspend fun getSystemInfoResult(address: String): SystemInfoResult { + private suspend fun getSystemInfoResult(address: RedirectInfo): SystemInfoResult { logger.info { "Requesting public system info for $address" } val client = jellyfin.createApi( - baseUrl = address, + baseUrl = address.getAddress(), httpClientOptions = HttpClientOptions( followRedirects = false, connectTimeout = HTTP_TIMEOUT, @@ -154,12 +174,45 @@ public class RecommendedServerDiscovery constructor( ) } - /** - * Discover all servers in the [servers] flow and retrieve the public system information to assign a score. - * Returned servers are not ordered by score. Use [minimumScore] to automatically remove bad matches. - */ - public suspend fun discover( - servers: Collection, + private suspend fun getRedirectInfo(address: String): RedirectInfo? { + logger.info { "Requesting header info for $address" } + + val client = jellyfin.createApi( + baseUrl = address, + httpClientOptions = HttpClientOptions( + followRedirects = false, + connectTimeout = HTTP_TIMEOUT, + requestTimeout = HTTP_TIMEOUT, + socketTimeout = HTTP_TIMEOUT, + ), + ) + + val info = try { + val response = client.head(pathTemplate = "") + Result.success(response) + } catch (err: TimeoutException) { + logger.debug(err) { "Could not connect to $address" } + Result.failure(err) + } catch (err: ApiClientException) { + logger.debug(err) { "Unable to get response from $address" } + Result.failure(err) + } + + // get the Location header or exit + val location = info.getOrElse { return null }.getHeader(HttpHeaders.Location.lowercase()) ?: return null + + // only follow the redirect if on the same host + val locationUrl = URLBuilder(location).build() + if (locationUrl.isRelativePath) return RedirectInfo(address, buildUrl(address, location)) + val serverUrl = URLBuilder(address).build() + if (locationUrl.host == serverUrl.host) return RedirectInfo(address, location) + + // host didn't match + return null + } + + private suspend fun testAndScore( + servers: Collection, minimumScore: RecommendedServerInfoScore, ): Collection = withContext(Dispatchers.IO) { val semaphore = Semaphore(MAX_SIMULTANEOUS_RETRIEVALS) @@ -178,4 +231,33 @@ public class RecommendedServerDiscovery constructor( serverInfo.score.score >= minimumScore.score } } + + /** + * Discover all servers in the [servers] flow and retrieve the public system information to assign a score. + * Returned servers are not ordered by score. Use [minimumScore] to automatically remove bad matches. + */ + public suspend fun discover( + servers: Collection, + minimumScore: RecommendedServerInfoScore, + followRedirects: Boolean = false, + ): Collection = withContext(Dispatchers.IO) { + val semaphore = Semaphore(MAX_SIMULTANEOUS_RETRIEVALS) + var allServers = servers.map { RedirectInfo(it) } + + if (followRedirects) { + val redirects = servers + .map { address -> + async { + semaphore.withPermit { + getRedirectInfo(address) + } + } + } + .awaitAll() + allServers = allServers.plus(redirects.filterNotNull()).distinctBy { it.getAddress() } + } + + logger.debug { "allServers = $allServers" } + testAndScore(allServers, minimumScore) + } } diff --git a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerInfo.kt b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerInfo.kt index cc18c2860..a41268a38 100644 --- a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerInfo.kt +++ b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerInfo.kt @@ -8,10 +8,13 @@ public data class RecommendedServerInfo( val score: RecommendedServerInfoScore, val issues: Collection, val systemInfo: Result, + val originalAddress: String? = null, ) { /** * The issues are ordered by importance. When showing a single issue to an end user you * normally want to show the first one. */ public fun firstIssueOrNull(): RecommendedServerIssue? = issues.firstOrNull() + + public fun isRedirect(): Boolean = originalAddress != null && originalAddress != address } diff --git a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerIssue.kt b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerIssue.kt index 0a8b3b04c..d4bce451d 100644 --- a/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerIssue.kt +++ b/jellyfin-core/src/commonMain/kotlin/org/jellyfin/sdk/discovery/RecommendedServerIssue.kt @@ -50,4 +50,9 @@ public sealed interface RecommendedServerIssue { * The system information response was slow. */ public data class SlowResponse(public val responseTime: Long) : RecommendedServerIssue + + /** + * The address was the result of a redirect + */ + public data class RedirectedResponse(public val originalAddress: String, public val finalAddress: String) : RecommendedServerIssue } diff --git a/samples/kotlin-cli/src/main/kotlin/org/jellyfin/sample/cli/command/Discover.kt b/samples/kotlin-cli/src/main/kotlin/org/jellyfin/sample/cli/command/Discover.kt index 9db651ce3..4e73e1dc7 100644 --- a/samples/kotlin-cli/src/main/kotlin/org/jellyfin/sample/cli/command/Discover.kt +++ b/samples/kotlin-cli/src/main/kotlin/org/jellyfin/sample/cli/command/Discover.kt @@ -39,7 +39,7 @@ class Discover( val candidates = jellyfin.discovery.getAddressCandidates(address) logger.info("Found ${candidates.size} candidates") - val servers = jellyfin.discovery.getRecommendedServers(candidates) + val servers = jellyfin.discovery.getRecommendedServers(candidates, followRedirects = true) for (server in servers) { buildString { append(server.address)